diff --git a/docs/README.md b/docs/README.md index 3d8d3cb7..83114900 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ Pickleball extends Cucumber with a dynamic feature-file language while preserving normal Cucumber behavior. The pages below describe the supported authoring model and link to real executable examples in [`maven-consumer-project`](consumer-project.md). -When these docs are materialized from the Maven dependency with `DiagnosticCli export-guidance`, links to `../maven-consumer-project/...` resolve to the version-matched, read-only reference snapshot exported beside the docs. Human readers can inspect those working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. Consumer AI agents should follow `.pickleball/AGENT-GUIDE.md` first and open a specific guide only when needed. +When these docs are materialized from the Maven dependency with Workbench `export-guidance`, links to `../maven-consumer-project/...` resolve to the version-matched, read-only reference snapshot exported beside the docs. Human readers can inspect those working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. Consumer AI agents should follow `.pickleball/AGENT-GUIDE.md` first and open a specific guide only when needed. ## Start here diff --git a/docs/agent/README.md b/docs/agent/README.md index 14968b77..5ef38667 100644 --- a/docs/agent/README.md +++ b/docs/agent/README.md @@ -18,9 +18,9 @@ This directory supports repository-native AI coding agents. It is not a runtime Agent adapters should remain small and point back to the canonical contract rather than copying the full project description. -The nested `/maven-consumer-project/AGENTS.md` is a dependency-owned guidance bootstrap plus a short discover-vs-isolate pointer. It materializes version-matched guidance, directs the consumer agent to `.pickleball/AGENT-GUIDE.md`, and states that agents discover with a diagnostic `mvn test`; MCP is optional if already connected, and agents do not self-register IDE MCP. Refresh/version/manifest semantics, authoring rules, configuration, diagnostics, and troubleshooting belong in the exported dependency guidance. +The nested `/maven-consumer-project/AGENTS.md` is a dependency-owned Workbench bootstrap plus a short Discover/Isolate/Confirm pointer. It materializes version-matched guidance through `PickleballWorkbenchLauncher export-guidance`, directs the consumer agent to `.pickleball/AGENT-GUIDE.md`, and tells agents to use Workbench `hint` / `discover` / `isolate` / `confirm`. Do not start the GUI. Do not register IDE MCP. Refresh/version/manifest semantics, authoring rules, configuration, diagnostics, and troubleshooting belong in the exported dependency guidance. -The nested `/maven-consumer-project/.github/copilot-instructions.md` is the same bootstrap-plus-pointer for IntelliJ Copilot Chat, which reads that file rather than `AGENTS.md`. +The nested `/maven-consumer-project/.github/copilot-instructions.md` is identical to `AGENTS.md` so Copilot Chat sees the same Workbench pointer. The nested `/maven-consumer-project/README.md` is ordinary sample-project documentation. It may point humans and agents at `AGENTS.md` for guidance export, but should not duplicate the AI guidance lifecycle. diff --git a/docs/agent/feature-map.md b/docs/agent/feature-map.md index 8c1f7678..fa669e76 100644 --- a/docs/agent/feature-map.md +++ b/docs/agent/feature-map.md @@ -8,7 +8,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab | Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records; `InvestigationHandoff` | `verifyProtocolIsolation`; `ControlBridgeClientTest`; `InvestigationHandoffTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.md`; `docs/diagnostic-reporting.md`; `pickleball-workbench/AGENTS.md` | | Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease / scenario name-tag filter / Text-Blocks editor toggle | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchSyncPlanner`; `WorkbenchSyncInputs`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `workbench_diagnostic_catalog`; `workbench_diagnostic_run`; `workbench_diagnostic_summary`; `workbench_investigation_emit`; `tools.dscode.workbench.player`; `LiveEditorView`; `tools.dscode.workbench.catalog`; `ScenarioFilter`; `ConsumerFeatureCatalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; `FeaturePickerPanel`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `ScenarioFilterTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `LiveEditorViewTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `InvestigationHandoffTest`; `WorkbenchSynchronizerTest`; `WorkbenchSyncPlannerTest`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `docs/consumer-agent-guide.md`; `pickleball-workbench/AGENTS.md` | | Consumer-side Control Bridge and live investigation | worker-side `pickleball-control-api/src/main/java/tools/dscode/control/bridge`; core `ControlRuntime`; protocol `tools.dscode.control.protocol`; controller client `tools.dscode.workbench.bridge.ControlBridgeClient`; endpoints `/v1/status`, `/v1/scenarios`, `/v1/events`, `/v1/pause`, `/v1/resume`, `/v1/steps/execute`, `/v1/mappings/*`, `/v1/browser/*`, `/v1/services/call`, `/v1/breakpoints*`, `/v1/step-overrides*` | controller-only fake-server `ControlBridgeClientTest`; `control-bridge.feature` tagged `@control-bridge`; `ControlBridgeTestSteps.java`; Workbench `worker-check` / `live-check` | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` | -| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction | `PickleballWorkbenchLauncherTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` | +| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `WorkbenchAgentCommands`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction; agent verbs `export-guidance`, `hint`, `discover`, `isolate`, `confirm` | `PickleballWorkbenchLauncherTest`; `WorkbenchAgentCommandsTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md`; `docs/consumer-agent-guide.md` | | Step Override runtime and Workbench authoring | `src/main/java/tools/dscode/control/override`; `src/main/java/io/cucumber/core/runner/StepOverrideDispatcher.java`; `NPickleStepTestStepFactory`; `src/main/aspectj/tools/dscode/control/override/StepOverrideLifecycleAspect.aj`; bridge `/v1/step-overrides*`; `ControlBridgeClient`; `WorkbenchLiveSession`; MCP `workbench_step_override_*` | `StepOverrideCompilerTest`; `StepOverrideChecks`; `StepOverrideBridgeTestSteps`; `@step-override`; Workbench `live-check`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest` | `docs/step-overrides.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` | | Dynamic control API and semantic hooks | `pickleball-control-api/src/main/java/tools/dscode/control/api`; `src/main/java/tools/dscode/common/control`; `src/main/aspectj/tools/dscode/common/control/ControlRuntimeAspect.aj`; search `DynamicControl`, `MappingControl`, `ElementControl`, `ServiceCallControl`, `ControlHook` | `DynamicControlApiChecks.java`; `ControlRuntimeObserverChecks.java`; `internal-framework-java-checks.feature`; `control-bridge.feature` | `docs/dynamic-control-api.md` | | Pickleball-native element inspection | `ElementControl.java`; `ElementInspection.java`; `ElementEvidence.java`; `ExecutionDictionary.java`; `DefinitionContext.java`; `BrowserSteps.getCurrentDriverIfPresent`; bridge `/v1/browser/elements`; Workbench `workbench_element_inspect` | `@control-bridge`; custom element/category consumer scenarios | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `docs/custom-element-definitions.md` | @@ -18,8 +18,8 @@ This file maps consumer-visible capabilities to implementation anchors, executab | Selenium navigation and interaction | `BrowserSteps.java`; `NavigationSteps.java`; `ElementWrapper.java`; `HumanInteractions.java`; `SeleniumUtils.java`; `DriverConstruction.java`; bundled `META-INF/pickleball/configs/CHROME_HEADLESS.yaml`; `ParsingMap.initializeConfigs` fallback | `navigation.feature`; `forms-dynamic-steps.feature`; `dialogs.feature`; `ChromeHeadlessConfigChecks.java`; browser test-site pages | `docs/dynamic-steps.md`; `docs/custom-element-definitions.md`; `docs/configuration.md`; `docs/config-files-and-resource-mapping.md` | | Custom element definitions/catalog context | `ExecutionDictionary.java`; `ElementMatch.java`; consumer `PickleballTests.java`; search `category(`, `inheritsFrom` | `catalog-context.feature`; `forms-dynamic-steps.feature`; `site/catalog.html` | `docs/custom-element-definitions.md`; `docs/config-files-and-resource-mapping.md` | | Mapping, ParsingMap/NodeMap, templates/directives | `MappingSteps.java`; `FileAndDataParsing.java`; `MappingProcessor.java`; `QuoteParser.java`; `NodeMap.java`; `ParsingMap.java`; `ValueFormatting.java`; `common/dataelements` | `mapping-and-resources.feature`; `mapping-value-type-preservation.feature`; `scenario-data-references.feature`; Data Element features; `QuoteParserChecks.java`; internal Java checks | `docs/mapping-and-templating.md`; `docs/data-values-and-elements.md`; `docs/data-element-query-runtime.md`; `docs/config-files-and-resource-mapping.md` | -| Configuration/profiles/RunVars | `PKB_props.java`; `PickleballProfiles.java`; `PkbPropertyValueNormalizer.java`; `ParallelCountEstimator.java`; runner/config classes; search `pkb_profile`, `pkb_runvars`, `pkb_run_profile`, `pkb_configpath`, `pkb_parallel=auto` | `configuration-system-properties.feature`; `ProfileConfigurationChecks.java`; `ParallelCountEstimatorChecks.java`; consumer properties/profile examples | `docs/configuration.md`; `docs/getting-started.md`; `docs/ai-run-configuration.md`; `docs/consumer-project.md` | -| Consumer guidance export/reference snapshot | `DiagnosticCli.java`; `gradle/consumer-guidance.gradle`; `scripts/sync_consumer_guidance.py`; `maven-consumer-project/AGENTS.md`; `maven-consumer-project/.github/copilot-instructions.md`; search `export-guidance`, `discover-hint`, `GUIDANCE-MANIFEST.json`, `.pickleball/investigations` | `PickleballGuidanceChecks.java`; consumer guidance contract checks | `docs/consumer-agent-guide.md`; `docs/consumer-project.md` | +| Configuration/profiles/RunVars | `PKB_props.java`; `PickleballProfiles.java`; `PkbPropertyValueNormalizer.java`; `ParallelCountEstimator.java`; `AgentBrowserLadder.java`; `AgentDiscoverPlanner.java`; runner/config classes; search `pkb_profile`, `pkb_runvars`, `pkb_run_profile`, `pkb_configpath`, `pkb_parallel=auto` | `configuration-system-properties.feature`; `ProfileConfigurationChecks.java`; `ParallelCountEstimatorChecks.java`; `AgentBrowserLadderChecks.java`; consumer properties/profile examples | `docs/configuration.md`; `docs/getting-started.md`; `docs/ai-run-configuration.md`; `docs/consumer-project.md` | +| Consumer guidance export/reference snapshot | `DiagnosticCli.java`; `PickleballWorkbenchLauncher` `export-guidance`; `gradle/consumer-guidance.gradle`; `scripts/sync_consumer_guidance.py`; `maven-consumer-project/AGENTS.md`; `maven-consumer-project/.github/copilot-instructions.md`; search `export-guidance`, `hint`, `discover-hint`, `GUIDANCE-MANIFEST.json`, `.pickleball/investigations` | `PickleballGuidanceChecks.java`; consumer guidance contract checks | `docs/consumer-agent-guide.md`; `docs/consumer-project.md` | | Consumer AI-agent pointer-eval harness | `maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`; tag `@agent-pointer-eval` (optional `@agent-pointer-eval-pass` / `@agent-pointer-eval-fail`) | opt-in mixed pass/fail; must not carry `@all`, `@regression`, or other Maven suite-profile tags; `scripts/verify_agent_contract.py` | `docs/consumer-agent-guide.md` | | Diagnostic reporting and controlled reruns | `src/main/java/tools/dscode/common/reporting/diagnostic`; `InvestigationHandoff`; diagnostic aspects; `DiagnosticCli.java`; `emit-investigation`; `VisualFingerprintComparator.java`; `DiagnosticRunComparator.java`; catalog/summary `runProfile` | `DiagnosticReportingChecks.java`; `Diagnostic213CompletionChecks.java`; `InvestigationHandoffChecks.java`; diagnostic features | `docs/diagnostic-reporting.md`; `docs/ai-diagnostic-reporting-plan.md`; `docs/ai-run-configuration.md`; root `AGENTS.md` | | Nested steps/block conditionals | search `Nested`, `Conditional`, `Block`, `Condition` in core implementation | `nested-and-block-conditionals.feature` | `docs/nested-steps.md`; `docs/block-conditionals.md` | @@ -48,6 +48,6 @@ The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`, `WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. A `FAILED` `executeStep` result leaves that same paused worker available; it is not a worker-ending error. Step Override source is compiled and loaded worker-side. Normal live calls do not invoke Maven/Gradle, resynchronize, or restart the worker. -The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). Consumer AI agents discover unknown or multi-scenario failures with a diagnostic `mvn test` and `run-catalog.json` (zero MCP is enough). Live `workbench_*` MCP tools are an optional alias when already connected; agents must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with those tools or a Workbench CLI session; missing tools is a dead-end to tell the human, not a skip of Discover. UI mode may write a localhost attach endpoint to `.pickleball/workbench/attach.json` so a watcher can join a human GUI session instead of starting a second Workbench; that attach file is not the consumer-agent path for this release. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system. +The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). Consumer AI agents use Workbench `discover` / `isolate` / `confirm` as the one front door. Live `workbench_*` MCP tools are an optional alias when already connected; agents must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with Workbench CLI `isolate` or already-connected `workbench_*` tools. If isolate cannot obtain a live worker, tell the human that Workbench CLI isolate failed. UI mode may write a localhost attach endpoint to `.pickleball/workbench/attach.json` so a watcher can join a human GUI session instead of starting a second Workbench; that attach file is not the consumer-agent path for this release. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system. For this boundary, scenario validation is limited to the affected `@control-bridge` and/or `@step-override-bridge` tags with `pkb_parallel=80` where practical. Do not run `@all` for Workbench isolation changes. diff --git a/docs/agent/repository-index.md b/docs/agent/repository-index.md index 7ff607f3..5b4c9f58 100644 --- a/docs/agent/repository-index.md +++ b/docs/agent/repository-index.md @@ -237,13 +237,17 @@ This inventory helps coding agents discover relevant files. It does not replace - `src/main/java/tools/dscode/common/mappings/ScenarioMapping.java` - `src/main/java/tools/dscode/common/mappings/StepMapping.java` - `src/main/java/tools/dscode/common/mappings/ValueFormatting.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadder.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlanner.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ConfigurationProvenance.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/ConsumerMavenTestRunner.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticIndexRebuilder.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticReporter.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticRunComparator.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticRuntime.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ExplicitReportRegistry.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/LastDiscoverSnapshot.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ReportRetentionPolicy.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ScenarioIdentity.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/SourceProvenance.java` @@ -365,6 +369,8 @@ This inventory helps coding agents discover relevant files. It does not replace - `src/main/java/tools/dscode/coredefinitions/UtilitySteps.java` - `src/main/java/tools/dscode/cucumberextended/utilities/StringUtilities.java` - `src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java` +- `src/main/java/tools/dscode/launcher/WorkbenchAgentCommands.java` +- `src/main/java/tools/dscode/launcher/WorkbenchCommandLine.java` - `src/main/java/tools/dscode/misc/DummySteps.java` - `src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java` - `src/main/java/tools/dscode/parallelutilities/Stagger.java` @@ -389,8 +395,11 @@ This inventory helps coding agents discover relevant files. It does not replace ## Framework tests +- `src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderTest.java` +- `src/test/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlannerTest.java` - `src/test/java/tools/dscode/control/override/StepOverrideCompilerTest.java` - `src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java` +- `src/test/java/tools/dscode/launcher/WorkbenchAgentCommandsTest.java` - `src/test/java/tools/dscode/parallelutilities/ParallelCountEstimatorTest.java` - `src/test/java/tools/dscode/testengine/DynamicSuiteBootstrapWorkbenchRootTest.java` @@ -484,6 +493,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/discover/LastDiscoverSnapshot.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchCallContext.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLease.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLeaseSnapshot.java` @@ -547,6 +557,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/InvestigationHandoffTest.java` +- `pickleball-workbench/src/test/java/tools/dscode/workbench/discover/LastDiscoverSnapshotTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/lease/WorkbenchControlLeaseTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/mapping/MappingValueCodecTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchAttachServerTest.java` @@ -595,6 +606,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `maven-consumer-project/src/test/java/tools/dscode/common/driver/ChromeHeadlessConfigChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/mappings/MappingDataRefactorChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/mappings/QuoteParserChecks.java` +- `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/Diagnostic213CompletionChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/DiagnosticReportingChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/InvestigationHandoffChecks.java` @@ -603,6 +615,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `maven-consumer-project/src/test/java/tools/dscode/common/util/datetime/BusinessTimePostModifierChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/coredefinitions/DataTableConversionChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/coredefinitions/ModularScenariosChecks.java` +- `maven-consumer-project/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/parallelutilities/ParallelCountEstimatorChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/testengine/PkbPropertyValueNormalizerChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/testengine/ProfileConfigurationChecks.java` diff --git a/docs/ai-diagnostic-reporting-plan.md b/docs/ai-diagnostic-reporting-plan.md index f8891e79..c80ec593 100644 --- a/docs/ai-diagnostic-reporting-plan.md +++ b/docs/ai-diagnostic-reporting-plan.md @@ -132,13 +132,13 @@ For a source-only fix, reuse the retained RunVars unchanged and omit `pkb_change ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print this list. The agent-facing entry is Pickleball Workbench. See `docs/diagnostic-reporting.md` for evidence use, `docs/ai-run-configuration.md` for controlled execution, and `docs/diagnostic-lineage-metadata.md` for investigation metadata. diff --git a/docs/ai-run-configuration.md b/docs/ai-run-configuration.md index 6201a14e..f38c6d3f 100644 --- a/docs/ai-run-configuration.md +++ b/docs/ai-run-configuration.md @@ -323,18 +323,18 @@ When operating in a consumer project: ## AI agents -Set a **complete** Discover `pkb_runvars` rather than a partial overlay: +The agent-facing entry is Pickleball Workbench (`hint`, `discover`, `isolate`, `confirm`). Set a **complete** Discover `pkb_runvars` rather than a partial overlay. Workbench `hint` prints the browser-ladder result and estimated integer parallel count. The browser ladder keeps a remote project `pkb_browser` (`SAUCE_*` / `GRID_*` / `REMOTE_*`); otherwise it prefers `CHROME_HEADLESS`. Unused Sauce/Grid yaml files are not auto-selected. ```text -pkb_browser=CHROME_HEADLESS +pkb_browser= pkb_parallel= pkb_reportingmode=diagnostic pkb_loglevel=warn pkb_reportretention=failed ``` -plus the narrowest useful `pkb_tags` / `pkb_name`. `DiagnosticCli discover-hint` prints the estimated integer parallel count for the current JVM. Multi-scenario Discover/Confirm must use headless Chrome and high parallelism. Isolate / live Workbench stays one paused scenario. +plus the narrowest useful `pkb_tags` / `pkb_name`. Multi-scenario Discover/Confirm use that high parallelism. Isolate / live Workbench stays one paused scenario. -After the run, inspect `pkb_run_profile` from `run-catalog.json`, `run-index.json`, or `summary.json`. That output is the complete resolved RunVar list after inheritance (glue/features/data/call/component/configpath) and after `pkb_parallel=auto` is stamped as an integer. Do not assume omitted `pkb_runvars` keys equal project `pickleball.properties` — optional keys such as headed Chrome, `pretty`, and `pkb_tags=@all` do not leak into a controlled run, which is why agents must set the Discover keys explicitly. +After Discover, inspect `pkb_run_profile` from `run-catalog.json`, `run-index.json`, or `summary.json`. Isolate and confirm replay that retained profile through `pkb_runvars`. If there is no prior Discover snapshot, Workbench says so; it does not silently re-resolve from project defaults. Never supply `pkb_run_profile` as input. Workbench MCP `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` return the same retained `runProfile` when present. The consumer worker resolves the same snapshot internally through `PickleballRunner`; it does not accept `pkb_run_profile` as input. diff --git a/docs/configuration.md b/docs/configuration.md index 456c4a5c..6e28fb04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -356,7 +356,7 @@ max(2, min(availableProcessors, floor(maxMemoryMB / 512), 24)) Chrome workers are RAM-heavy, so a 32-core / 64GiB box does not blindly pick 32 workers; the hard cap is 24, and heap can cap lower. Tiny heaps resolve to 2. An explicit numeric `pkb_parallel` is never overwritten. Omitting `pkb_parallel` does not enable parallel execution. -The resolved integer is stamped into the final RunVars and `pkb_run_profile`. `DiagnosticCli discover-hint` prints that estimated number in the recommended Discover `pkb_runvars` command. +The resolved integer is stamped into the final RunVars and `pkb_run_profile`. Workbench `hint` prints that estimated number in the recommended Discover `pkb_runvars` command. ## Bundled `CHROME_HEADLESS` @@ -367,6 +367,16 @@ The resolved integer is stamped into the final RunVars and `pkb_run_profile`. `D The bundled headless config uses `--headless=new`, a fixed `--window-size=1920,1080`, no `MAXIMIZE`, and `QUIT_LOCAL_DRIVER`. Consumer `CHROME`, `EDGE`, `GRID`, and `SAUCE` yaml files are unchanged. Agents can set `pkb_browser=CHROME_HEADLESS` without copying yaml into the project. +## Agent Discover browser ladder + +Workbench Discover/Confirm do not blindly MUST-use `CHROME_HEADLESS` for every project: + +1. If `default_profile` / runner / retained `pkb_run_profile` `pkb_browser` is already a remote farm name (`SAUCE_*`, `GRID_*`, `REMOTE_*`, or clearly non-local), keep it. Those consumers run exclusively on the external farm. +2. Otherwise prefer `CHROME_HEADLESS` (consumer yaml if present, else the JAR-bundled config above). +3. If local headless cannot start and the project already defines and uses GRID/SAUCE/REMOTE as its `pkb_browser`, fall back to that project browser. Do not pick Sauce/Grid merely because unused yaml files exist in `configs/`. + +Isolate stays one scenario and does not raise `pkb_parallel`. + ## Cucumber aliases Pickleball synchronizes its main selection aliases with Cucumber properties, including: diff --git a/docs/consumer-agent-guide.md b/docs/consumer-agent-guide.md index 467090e8..25a8e3f5 100644 --- a/docs/consumer-agent-guide.md +++ b/docs/consumer-agent-guide.md @@ -2,45 +2,37 @@ This is the canonical AI-agent contract for projects that consume Pickleball as a Maven dependency. -A consumer project may contain only a short `AGENTS.md` bridge. That bridge can use Pickleball's `DiagnosticCli export-guidance` command to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. +A consumer project may contain only a short `AGENTS.md` bridge. That bridge uses Pickleball Workbench (`tools.dscode.launcher.PickleballWorkbenchLauncher`) `export-guidance` to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. ## Tool chooser -### Access probe +Pickleball Workbench is the one front door. It is a Java/Maven program, not an IDE feature and not a GUI requirement. Do not start the GUI. Do not register IDE MCP. -Prefer shell and Maven. This path is CLI/Maven-primary. Do not configure or self-register IDE MCP. Do not start an IDE-owned stdio server. +If `workbench_*` tools are already present in this session, use them as an alias for the same Isolate loop. Otherwise keep using Workbench CLI verbs. Missing `workbench_*` tools is not a reason to skip Discover. -If `workbench_*` tools are already present in this session, you may use them as an optional alias for the same live isolation loop. If they are absent, do not try to make the IDE own a stdio process. Continue with Discover using `mvn test` and the diagnostic JSON files. For Isolate, follow CLI/Workbench steps in the live loop when a Workbench session already exists; otherwise stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not silently use `mvn test` as isolate/debug. +From the consumer project, with Pickleball on the test classpath (`classpathScope=test`): -Use this order. Consumer AI agents for this Pickleball release discover failures with diagnostic `mvn test`. Live Workbench MCP is an optional alias when `workbench_*` tools are already connected. Do not start the Workbench GUI. +1. **Discover** — `PickleballWorkbenchLauncher discover` (optional `--tags` / `--name`). Workbench applies complete AI `pkb_runvars`: browser ladder, high/auto parallel, diagnostic, warn, failed retention. It wraps consumer `mvn test`. Do not start a live worker to run the whole suite. Then read `run-catalog.json` and the retained `pkb_run_profile`. +2. **Isolate** — `PickleballWorkbenchLauncher isolate` with `--tags` / `--name` for a known scenario. Replays the last Discover `pkb_run_profile` as `pkb_runvars` (never supply `pkb_run_profile` as input). One paused scenario; do not parallelize isolate. If isolate cannot obtain a live worker, tell the human that Workbench CLI isolate failed. +3. **Confirm** — `PickleballWorkbenchLauncher confirm` with the same Discover snapshot and narrow tags/name. +4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. -1. **Discover** — when you do not yet know which scenarios fail, or you need many/parallel scenarios: run one diagnostic `mvn test` with a **complete** `pkb_runvars` that always includes `pkb_browser=CHROME_HEADLESS`, a high `pkb_parallel` (the conservative JVM estimate printed by `discover-hint`, or `pkb_parallel=auto`), `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, plus the narrowest useful `pkb_tags`/`pkb_name`. Multi-scenario Discover and Confirm MUST use headless Chrome and high parallelism. Isolate / live Workbench stays one paused scenario; do not parallelize isolate. Use headed Chrome only when the scenario actually needs a window. Set those keys explicitly so project headed-chrome / pretty / `@all` defaults do not sneak in. Execution-context keys (`pkb_glue`, `pkb_features`, `pkb_datapath`, `pkb_callpath`, `pkb_componentpath`, `pkb_configpath`) may still inherit; after the run, read `pkb_run_profile` from `run-catalog.json` / `run-index.json` / `summary.json` as the complete resolved list, including inherited glue/features and the integer `pkb_parallel`. Do not assume omitted keys equal project pickleball.properties. This is not a skip of Workbench; it is how you find failures. Do not start a worker just to run the whole suite. After the run, open `run-catalog.json`, then only the relevant `run-index.json` / `summary.json`. If `workbench_*` tools already exist, you may instead read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary`. Do not ingest `events.jsonl` or screenshots to find which scenarios failed. Do not `workbench_worker_start` to read the catalog. -2. **Isolate / debug a known failing scenario** — after discovery has named the trouble spots, isolate those scenarios with live Workbench. If `workbench_*` tools are already present, use the live MCP loop below. If they are absent, do not invent IDE MCP and do not start an IDE-owned stdio server. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not keep using `mvn test` for isolation/debug. -3. **Confirm after isolation** — one bounded diagnostic `mvn test` with the same complete AI `pkb_runvars` (`pkb_browser=CHROME_HEADLESS`, high `pkb_parallel` when more than one scenario, `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, narrowest useful `pkb_tags`/`pkb_name`). -4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. Change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. - -Discover must work with zero MCP. Missing `workbench_*` tools is not a reason to skip Discover, and it is not a reason to self-register IDE MCP. A multi-scenario diagnostic test is a reason to run `mvn test` first; do not start a worker to run the whole suite. +`hint` (alias `discover-hint`) prints the recommended Discover `pkb_runvars` and `NEXT: run discover`. `mcp` and `ui` are host/human commands. Agents must not use `ui`. Hosts may already wire `mcp .`; that is optional host wiring, mentioned once, not an agent setup step. Do not copy consumer features into `.pickleball` as a sandbox. ### Live isolation loop -This loop isolates a **known** failing scenario after Discover. Hosts that already run Workbench MCP may wire `tools.dscode.launcher.PickleballWorkbenchLauncher` with `classpathScope=test` and args `mcp .` (not a GUI command). That is optional host wiring, not an agent setup step. Agents must not configure, self-register, or start an IDE-owned stdio MCP process. - -From the consumer project, with Pickleball on the test classpath: +This loop isolates a **known** failing scenario after Discover. Prefer Workbench CLI `isolate`. If `workbench_*` tools are already in this session, they are the same loop (`workbench_sync` / `workbench_worker_start` must use the Discover snapshot replayed as `pkb_runvars`). -1. If `workbench_*` tools are already in this session, continue with `workbench_sync` / `workbench_worker_start`. If they are absent, do not invent IDE MCP. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. -2. Call `workbench_sync` once. The agent must call it; Workbench does not auto-watch. Full compile when there is no live classpath or when Java/`pom`/dependencies changed; resources-only for feature/config/data; skip when unchanged. Live buffer edits need no sync. -3. `workbench_worker_start` — reuse the compiled live classpath; do not rebuild to start a worker. -4. `workbench_request_control` -5. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. -6. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`; screenshot bytes are expensive in agent context. -7. When you need a retained evidence pack, run **one** diagnostic `mvn test` with `pkb_runvars` (below). Read that pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` instead of globbing `reports/diagnostic-runs`. -8. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. Do not paste the report body, cause/fix essays, or screenshots into the chat panel. +1. Workbench `isolate` (CLI) or, when already connected, `workbench_sync` then `workbench_worker_start`. +2. `workbench_request_control` when using the MCP alias. +3. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. +4. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`. +5. Confirm with Workbench `confirm`. Read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` when those tools already exist. +6. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. -`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Insert, nest, or retry in the same paused browser/Mapping state. MCP `isError=true` is for controller/runtime problems such as a missing paused worker; do not restart the worker merely because a step failed. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). - -Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`); they do not require Maven. +`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`). ### Generated trees are not the project @@ -54,7 +46,7 @@ Worker restart without rebuild already exists (`workbench_worker_restart`). Step Keep first-read small. After a successful export: 1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Stay in this guide's tool chooser: Discover with a diagnostic `mvn test` when the failing scenario is unknown (zero MCP is enough). Isolate a known failure with already-connected `workbench_*` tools or a Workbench CLI session; do not self-register IDE MCP. +2. Stay in this guide's tool chooser: Workbench `discover` when the failing scenario is unknown. Isolate a known failure with Workbench `isolate`, or with already-connected `workbench_*` tools as the same loop. Do not start the GUI. 3. Inspect the **real** consumer `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. 4. Open a specific exported guide only when that topic is needed, for example `docs/dynamic-steps.md`, `docs/diagnostic-reporting.md`, `docs/configuration.md`, or `docs/ai-run-configuration.md`. 5. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. @@ -146,25 +138,25 @@ Never supply `pkb_run_profile` or `pkb_run_profile.` as input. They are When you launch Pickleball tests and the intended execution settings are known, use `pkb_runvars` as the authoritative input. Put intentional tag/name selection, browser, evidence/logging controls, and other non-secret RunVar changes inside `pkb_runvars`; do not default to ambient optional project settings or separate JVM `-Dpkb_*` RunVars. Use `pkb_profile` or ordinary JVM RunVar overrides only when the task specifically tests those configuration semantics or the user asks for them. Keep protected secrets and diagnostic lineage outside `pkb_runvars`. -For an agent's bounded confirmation `mvn test` (not the human runner defaults), include diagnostic evidence controls, headless Chrome, and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars`: +For an agent's bounded confirmation (not the human runner defaults), include diagnostic evidence controls, the browser ladder (keep a remote `pkb_browser`; otherwise prefer `CHROME_HEADLESS`), and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars` keys: ```text -pkb_browser=CHROME_HEADLESS +pkb_browser= pkb_parallel= pkb_reportingmode=diagnostic pkb_loglevel=warn pkb_reportretention=failed ``` -Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. `DiagnosticCli discover-hint` prints the estimated integer `pkb_parallel` for the current JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. +Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. Workbench `hint` prints the estimated integer `pkb_parallel` and the selected browser for the current project/JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. -These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after a live-loop isolation: +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after isolation: ```text -mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_parallel=auto, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +PickleballWorkbenchLauncher confirm --tags=@the-failing-tag --name='The failing scenario' ``` -After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. +After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. Isolate and confirm replay that retained profile through `pkb_runvars`; they do not silently re-resolve from project defaults. A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: @@ -222,21 +214,31 @@ After isolation and the diagnostic rerun, emit a small human handoff. JSON is th ## Diagnostic utility commands -From a Maven consumer where Pickleball is on the test classpath: +The agent-facing name is Workbench. From a Maven consumer where Pickleball is on the test classpath: + +```text +PickleballWorkbenchLauncher export-guidance .pickleball +PickleballWorkbenchLauncher hint +PickleballWorkbenchLauncher discover [--tags ] [--name ] +PickleballWorkbenchLauncher isolate [--tags ] [--name ] +PickleballWorkbenchLauncher confirm [--tags ] [--name ] +``` + +`DiagnosticCli` remains the implementation behind export-guidance/hint and the comparison/rebuild utilities: ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print that DiagnosticCli list and state that Workbench is the agent entry. -Use `guidance` to print this guide, `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, and `discover-hint` for the complete diagnostic `mvn test` `pkb_runvars` one-liner (headless Chrome, estimated `pkb_parallel`, diagnostic evidence controls) plus `run-catalog.json` / `pkb_run_profile` next step. Prefer `DiagnosticCli` over constructing Maven classpaths and JShell scripts for routine diagnostic operations. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. +Use Workbench `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, `hint` for the complete Discover `pkb_runvars` (browser ladder, estimated `pkb_parallel`, diagnostic evidence controls), and `discover` / `isolate` / `confirm` for the turnkey loop. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. ## Controlled diagnostic reruns @@ -300,14 +302,16 @@ If the consumer is nested inside the Pickleball source repository, repository-le For a normal external consumer, do not assume those core files exist. -## Maintainer pointer-eval harness +## Maintainer-only: pointer-eval harness + +This section is for Pickleball maintainers. It is not the product suite and is not first-read for consumer agents. -Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. +Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses Workbench discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. During an eval, still follow Discover / Isolate / Confirm; do not ignore canned fails. Run it explicitly: ```text -mvn test -Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS" +PickleballWorkbenchLauncher discover --tags=@agent-pointer-eval ``` -or `-Dpkb_tags=@agent-pointer-eval`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. +or `-Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS"`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. diff --git a/docs/consumer-project.md b/docs/consumer-project.md index befba07c..48e16392 100644 --- a/docs/consumer-project.md +++ b/docs/consumer-project.md @@ -9,7 +9,7 @@ The nested consumer intentionally keeps its own Markdown minimal. Detailed usage From a Maven consumer with Pickleball on the test classpath: ```powershell -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.common.reporting.diagnostic.DiagnosticCli" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball" +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball" ``` Then read or browse: @@ -26,7 +26,7 @@ Rerun export before Pickleball work even when `.pickleball` already exists. A su Compatibility note: an older Pickleball release whose exporter predates the manifest lifecycle may leave newer files or a newer manifest behind after a downgrade. Those leftovers are not authoritative for the downgraded dependency; prefer the dependency actually resolved on the test classpath and the files freshly exported by that dependency. -AI agents should read `.pickleball/AGENT-GUIDE.md` first after a successful export. Agents discover which scenarios fail with a diagnostic `mvn test`; MCP is optional if already connected, and agents do not self-register IDE MCP. That guide's tool chooser is the agent path: one diagnostic `mvn test` to discover which scenarios fail when that is still unknown; live Workbench only when `workbench_*` tools are already available or a Workbench CLI session exists; one bounded diagnostic confirmation; then edits to the real consumer source. Do not treat `.pickleball/maven-consumer-project/` as the project under test, and do not dump `docs/README.md` or the whole snapshot into first-read context. Human readers can start with `.pickleball/docs/README.md`; links from those guides to `maven-consumer-project` resolve to the exported version-matched reference files. +AI agents should read `.pickleball/AGENT-GUIDE.md` first after a successful export. Workbench is the one front door: `discover`, `isolate`, and `confirm`. Do not start the GUI. Do not register IDE MCP. If `workbench_*` tools are already connected they are an Isolate alias. Do not treat `.pickleball/maven-consumer-project/` as the project under test, and do not dump `docs/README.md` or the whole snapshot into first-read context. Human readers can start with `.pickleball/docs/README.md`; links from those guides to `maven-consumer-project` resolve to the exported version-matched reference files. ## Version-matched reference snapshot @@ -152,10 +152,10 @@ mvn test -Dpkb_tags="@forms and @state-assertions" mvn test -Dpkb_tags="@workflow and @nested-steps and not @block-conditionals" ``` -Human `PickleballTests` defaults remain `pretty` and `@all`. Agents launching a bounded confirmation should not reuse those defaults. Use a separate `pkb_runvars` command, for example: +Human `PickleballTests` defaults remain `pretty` and `@all`. Agents launching a bounded confirmation should not reuse those defaults. Use Workbench `confirm`, or a separate `pkb_runvars` command that honors the browser ladder: ```bash -mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_parallel=auto, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +PickleballWorkbenchLauncher confirm --tags=@the-failing-tag --name='The failing scenario' ``` After the run, read `pkb_run_profile` from `run-catalog.json` / `run-index.json` / `summary.json`. That is the complete resolved RunVar list. Do not assume omitted `pkb_runvars` keys equal project `pickleball.properties`. @@ -235,7 +235,8 @@ Do not recursively ingest an entire run. ### Diagnostic CLI ```text -DiagnosticCli discover-hint +PickleballWorkbenchLauncher hint +PickleballWorkbenchLauncher discover DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli emit-investigation diff --git a/docs/diagnostic-reporting.md b/docs/diagnostic-reporting.md index 449fd205..68d2c003 100644 --- a/docs/diagnostic-reporting.md +++ b/docs/diagnostic-reporting.md @@ -114,19 +114,19 @@ See `docs/ai-run-configuration.md` and `docs/diagnostic-lineage-metadata.md`. ## Diagnostic CLI -Supported command-line operations: +The agent-facing entry is Pickleball Workbench. DiagnosticCli remains the implementation behind export-guidance/hint and the comparison utilities: ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print this list and state that Workbench is the agent entry. Prefer `DiagnosticCli` over custom Maven-classpath/JShell workflows for routine comparison and recovery. diff --git a/docs/pickleball-workbench.md b/docs/pickleball-workbench.md index 128dea62..459aba95 100644 --- a/docs/pickleball-workbench.md +++ b/docs/pickleball-workbench.md @@ -45,7 +45,23 @@ META-INF/pickleball/workbench/pickleball-workbench.jar Run the small launcher from the consumer test classpath. For Maven consumers, this command requires no cache path, separate Workbench dependency, or separately selected version. -Hosts that already run Workbench MCP can launch it from the consumer test classpath: +Workbench is the consumer AI-agent front door. Agents use `export-guidance`, `hint`, `discover`, `isolate`, and `confirm`. Do not start the GUI. Do not register IDE MCP. + +```bash +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ + -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ + -Dexec.classpathScope=test \ + "-Dexec.args=export-guidance .pickleball" +``` + +```bash +"-Dexec.args=hint" +"-Dexec.args=discover --tags=@smoke" +"-Dexec.args=isolate --name='The failing scenario'" +"-Dexec.args=confirm --tags=@smoke" +``` + +`mcp` and `ui` remain host/human commands. Hosts that already run Workbench MCP can launch it from the consumer test classpath: ```bash mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ @@ -58,7 +74,7 @@ mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -Humans who want the Swing player can pass `ui .` instead. With no launcher arguments, `ui` and the current directory are selected automatically for that human default. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."`. Agents discover unknown failures with a diagnostic `mvn test`. Headless `mcp .` is optional host wiring when `workbench_*` tools are already connected; agents must not self-register IDE MCP. Agents for this release should not use the GUI, `ui .`, or `.pickleball/workbench/attach.json` as their path; see `.pickleball/AGENT-GUIDE.md`. +Humans who want the Swing player can pass `ui .` instead. With no launcher arguments, `ui` and the current directory are selected automatically for that human default. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."`. Headless `mcp .` is optional host wiring when `workbench_*` tools are already connected; agents must not self-register IDE MCP. Agents for this release should not use the GUI, `ui .`, or `.pickleball/workbench/attach.json` as their path; see `.pickleball/AGENT-GUIDE.md`. Gradle consumers can expose the same dependency-owned launcher without resolving a cache path or adding a Workbench dependency: @@ -321,7 +337,7 @@ Start the lightweight non-Spring MCP server for a consumer project. This is opti java -jar $workbenchJar mcp ".\maven-consumer-project" ``` -Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. That launcher is optional host wiring for environments that already run MCP. Agents discover unknown failures with a diagnostic `mvn test` and `run-catalog.json` (zero MCP is enough). They must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with already-connected `workbench_*` tools or a Workbench CLI session. Do not document or use the Swing GUI, `ui .`, or `.pickleball/workbench/attach.json` as the agent path. +Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. That launcher is optional host wiring for environments that already run MCP. Agents use Workbench `discover` / `isolate` / `confirm`. They must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with Workbench CLI `isolate` or already-connected `workbench_*` tools. Do not document or use the Swing GUI, `ui .`, or `.pickleball/workbench/attach.json` as the agent path. The server uses the official Java MCP SDK core and stdio transport with the Jackson 2 JSON adapter. MCP dependencies are Workbench-only and are shaded into the executable companion. Workbench deliberately does not use Spring Boot, Spring Framework, Spring AI, WebMVC, or Tomcat. diff --git a/maven-consumer-project/.github/copilot-instructions.md b/maven-consumer-project/.github/copilot-instructions.md index ea45b2ab..c3436469 100644 --- a/maven-consumer-project/.github/copilot-instructions.md +++ b/maven-consumer-project/.github/copilot-instructions.md @@ -1,7 +1,5 @@ -Before any Pickleball work, run `mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.common.reporting.diagnostic.DiagnosticCli" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball"` and, after a successful export, follow `.pickleball/AGENT-GUIDE.md`. +Before any Pickleball work, run `mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball"` and, after a successful export, follow `.pickleball/AGENT-GUIDE.md`. -To find which scenarios fail, run one diagnostic `mvn test` with `pkb_runvars` (parallel is OK when the project supports it). That discovery run is not a skip of Workbench. Do not skip Workbench. +Use Workbench `hint`, `discover`, `isolate`, and `confirm` for Discover / Isolate / Confirm. Do not start the GUI. Do not register IDE MCP. -To isolate a known failure, use live Workbench only if `workbench_*` tools are already available, or follow AGENT-GUIDE's CLI/Workbench steps. Do not register MCP with the IDE or start an IDE-owned stdio server. If `workbench_*` tools are already in this session, use them for isolate. - -Do not start the GUI. Do not start a worker to run the whole suite. +If `workbench_*` tools are already in this session, they are an alias for the same Isolate loop. diff --git a/maven-consumer-project/AGENTS.md b/maven-consumer-project/AGENTS.md index ea45b2ab..c3436469 100644 --- a/maven-consumer-project/AGENTS.md +++ b/maven-consumer-project/AGENTS.md @@ -1,7 +1,5 @@ -Before any Pickleball work, run `mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.common.reporting.diagnostic.DiagnosticCli" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball"` and, after a successful export, follow `.pickleball/AGENT-GUIDE.md`. +Before any Pickleball work, run `mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball"` and, after a successful export, follow `.pickleball/AGENT-GUIDE.md`. -To find which scenarios fail, run one diagnostic `mvn test` with `pkb_runvars` (parallel is OK when the project supports it). That discovery run is not a skip of Workbench. Do not skip Workbench. +Use Workbench `hint`, `discover`, `isolate`, and `confirm` for Discover / Isolate / Confirm. Do not start the GUI. Do not register IDE MCP. -To isolate a known failure, use live Workbench only if `workbench_*` tools are already available, or follow AGENT-GUIDE's CLI/Workbench steps. Do not register MCP with the IDE or start an IDE-owned stdio server. If `workbench_*` tools are already in this session, use them for isolate. - -Do not start the GUI. Do not start a worker to run the whole suite. +If `workbench_*` tools are already in this session, they are an alias for the same Isolate loop. diff --git a/maven-consumer-project/src/test/java/com/example/pickleball/InternalFrameworkTestSteps.java b/maven-consumer-project/src/test/java/com/example/pickleball/InternalFrameworkTestSteps.java index 116fbc17..03a82a92 100644 --- a/maven-consumer-project/src/test/java/com/example/pickleball/InternalFrameworkTestSteps.java +++ b/maven-consumer-project/src/test/java/com/example/pickleball/InternalFrameworkTestSteps.java @@ -12,6 +12,7 @@ import tools.dscode.common.driver.ChromeHeadlessConfigChecks; import tools.dscode.common.mappings.MappingDataRefactorChecks; import tools.dscode.common.mappings.QuoteParserChecks; +import tools.dscode.common.reporting.diagnostic.AgentBrowserLadderChecks; import tools.dscode.common.reporting.diagnostic.Diagnostic213CompletionChecks; import tools.dscode.common.reporting.diagnostic.DiagnosticReportingChecks; import tools.dscode.common.reporting.diagnostic.InvestigationHandoffChecks; @@ -20,6 +21,7 @@ import tools.dscode.common.util.datetime.BusinessTemporalDeltaChecks; import tools.dscode.common.util.datetime.BusinessTimePostModifierChecks; import tools.dscode.coredefinitions.ModularScenariosChecks; +import tools.dscode.launcher.WorkbenchAgentCommandChecks; import tools.dscode.parallelutilities.ParallelCountEstimatorChecks; import tools.dscode.testengine.PkbPropertyValueNormalizerChecks; import tools.dscode.testengine.ProfileConfigurationChecks; @@ -68,7 +70,9 @@ public static void runDiagnosticReportingJavaTests() { DiagnosticReportingChecks.class, Diagnostic213CompletionChecks.class, PickleballGuidanceChecks.class, - InvestigationHandoffChecks.class + InvestigationHandoffChecks.class, + AgentBrowserLadderChecks.class, + WorkbenchAgentCommandChecks.class ); } finally { ReportRetentionPolicy.clearThreadOverride(); diff --git a/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderChecks.java b/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderChecks.java new file mode 100644 index 00000000..afeb401c --- /dev/null +++ b/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderChecks.java @@ -0,0 +1,57 @@ +package tools.dscode.common.reporting.diagnostic; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class AgentBrowserLadderChecks { + @Test + void remoteProjectBrowserIsKeptAndUnusedSauceYamlIsIgnored() throws Exception { + Path project = Files.createTempDirectory("pkb-browser-ladder-remote-"); + try { + Path resources = project.resolve("src/test/resources"); + Files.createDirectories(resources.resolve("configs")); + Files.writeString(resources.resolve("pickleball.properties"), "pkb_browser=SAUCE_CHROME\n"); + Files.writeString(resources.resolve("configs/GRID_CHROME.yaml"), "unused: true\n"); + + AgentBrowserLadder.Decision decision = AgentBrowserLadder.select(project); + assertEquals("SAUCE_CHROME", decision.browser()); + assertTrue(decision.remoteKept()); + } finally { + deleteTree(project); + } + } + + @Test + void localBrowserPrefersChromeHeadless() throws Exception { + Path project = Files.createTempDirectory("pkb-browser-ladder-local-"); + try { + Path resources = project.resolve("src/test/resources"); + Files.createDirectories(resources.resolve("configs")); + Files.writeString(resources.resolve("pickleball.properties"), "pkb_browser=chrome\n"); + Files.writeString(resources.resolve("configs/SAUCE_CHROME.yaml"), "unused: true\n"); + + AgentBrowserLadder.Decision decision = AgentBrowserLadder.select(project); + assertEquals("CHROME_HEADLESS", decision.browser()); + assertFalse(decision.remoteKept()); + assertNull(AgentBrowserLadder.fallbackIfHeadlessCannotStart(decision)); + } finally { + deleteTree(project); + } + } + + private static void deleteTree(Path root) throws Exception { + if (root == null || !Files.exists(root)) return; + try (var paths = Files.walk(root)) { + for (Path path : paths.sorted((a, b) -> b.compareTo(a)).toList()) { + Files.deleteIfExists(path); + } + } + } +} diff --git a/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/PickleballGuidanceChecks.java b/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/PickleballGuidanceChecks.java index 74e282f7..cbe2b072 100644 --- a/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/PickleballGuidanceChecks.java +++ b/maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/PickleballGuidanceChecks.java @@ -56,45 +56,45 @@ void dependencyPrintsCanonicalAgentGuide() { assertTrue(guide.contains("docs/pickleball-workbench.md")); assertTrue(guide.contains("DiagnosticCli help")); assertTrue(guide.contains("**Discover**")); - assertTrue(guide.contains("**Isolate / debug a known failing scenario**")); - assertTrue(guide.contains("Access probe")); - assertTrue(guide.contains("CLI/Maven-primary")); - assertTrue(guide.contains("self-register")); - assertTrue(guide.contains("IDE-owned stdio")); + assertTrue(guide.contains("**Isolate**")); + assertTrue(guide.contains("**Confirm**")); + assertTrue(guide.contains("Do not register IDE MCP")); assertTrue(guide.contains("tell the human")); assertTrue(guide.contains("run-catalog.json")); assertTrue(guide.contains("PickleballWorkbenchLauncher")); - assertTrue(guide.contains("does not auto-watch")); + assertTrue(guide.contains("does not auto-watch") || guide.contains("workbench_sync")); assertTrue(guide.contains("pkb_parallel")); - assertTrue(guide.contains("CHROME_HEADLESS")); + assertTrue(guide.contains("CHROME_HEADLESS") || guide.contains("browser ladder")); assertTrue(guide.contains("pkb_run_profile")); String chooser = guide.substring(0, guide.indexOf("Generated guidance lifecycle")); assertFalse(chooser.contains("attach.json")); assertFalse(chooser.contains("ui .")); - assertTrue(chooser.contains("This is not a skip of Workbench")); - assertTrue(chooser.contains("Do not keep using `mvn test` for isolation/debug")); + assertFalse(chooser.contains("This is not a skip of Workbench")); + assertFalse(chooser.contains("Do not skip Workbench")); + assertFalse(chooser.contains("MUST use headless Chrome")); + assertTrue(chooser.contains("one front door") || chooser.contains("Pickleball Workbench is the one front door")); String chooserList = guide.substring( guide.indexOf("## Tool chooser"), guide.indexOf("### Live isolation loop") ); - assertFalse(chooserList.contains("classpathScope")); + assertTrue(chooserList.contains("discover")); + assertTrue(chooserList.contains("isolate")); + assertTrue(chooserList.contains("confirm")); assertTrue(chooserList.contains("run-catalog.json")); - assertTrue(chooserList.contains("after discovery has named the trouble spots")); - assertTrue(chooserList.contains("Do not start a worker just to run the whole suite")); - assertTrue(chooserList.contains("CHROME_HEADLESS")); - assertTrue(chooserList.contains("pkb_parallel")); - assertTrue(chooserList.contains("do not parallelize isolate") - || chooserList.contains("Do not parallelize isolate")); + assertTrue(chooserList.contains("Do not start a live worker to run the whole suite") + || chooserList.contains("Do not start a live worker")); assertTrue(chooserList.contains("pkb_run_profile")); - assertTrue(guide.contains("Maintainer pointer-eval harness")); + assertFalse(chooserList.contains("Access probe")); + assertFalse(chooserList.contains("CLI/Maven-primary")); + assertTrue(guide.contains("Maintainer-only: pointer-eval harness")); assertTrue(guide.contains("@agent-pointer-eval")); String liveLoop = guide.substring( guide.indexOf("### Live isolation loop"), guide.indexOf("Generated guidance lifecycle") ); - assertTrue(liveLoop.contains("PickleballWorkbenchLauncher")); - assertTrue(liveLoop.contains("classpathScope=test")); - assertTrue(liveLoop.contains("optional host wiring")); + assertTrue(liveLoop.contains("isolate")); + assertTrue(liveLoop.contains("workbench_*") || liveLoop.contains("workbench_sync")); + assertTrue(liveLoop.contains("optional host wiring") || chooser.contains("optional host wiring")); } @Test @@ -121,6 +121,7 @@ void diagnosticCliHelpListsCommandsAndUnknownCommandsFailClearly() { assertTrue(help.contains("DiagnosticCli guidance")); assertTrue(help.contains("DiagnosticCli export-guidance")); assertTrue(help.contains("DiagnosticCli discover-hint")); + assertTrue(help.contains("Pickleball Workbench") || help.contains("PickleballWorkbenchLauncher")); assertTrue(help.contains("DiagnosticCli emit-investigation")); assertTrue(help.contains("DiagnosticCli compare-runs")); assertTrue(help.contains("DiagnosticCli compare-fingerprints")); @@ -166,7 +167,6 @@ void discoverHintPrintsDiagnosticMvnTestAndRunCatalogNext() { )); assertEquals("", errors.toString(StandardCharsets.UTF_8)); String text = output.toString(StandardCharsets.UTF_8); - assertTrue(text.contains("mvn test")); assertTrue(text.contains("pkb_runvars")); assertTrue(text.contains("pkb_browser=CHROME_HEADLESS")); assertTrue(text.contains("pkb_parallel=" + tools.dscode.parallelutilities.ParallelCountEstimator.estimate())); @@ -176,10 +176,8 @@ void discoverHintPrintsDiagnosticMvnTestAndRunCatalogNext() { assertTrue(text.contains("pkb_run_profile")); assertFalse(text.contains("pkb_parallel=80")); assertFalse(text.contains("when the project supports it")); - assertFalse(text.contains("when the project supports it")); - assertTrue(text.contains("reports/diagnostic-runs/run-catalog.json")); - assertTrue(text.contains("NEXT: run the diagnostic mvn test above, then open run-catalog.json")); - assertFalse(text.toLowerCase().contains("mcp")); + assertTrue(text.contains("NEXT: run discover")); + assertTrue(text.contains("Pickleball Workbench") || text.contains("Workbench")); } @Test @@ -195,8 +193,8 @@ void dependencyExportsVersionMatchedGuidanceAndManifest() throws Exception { assertEquals(0, status); String exportText = exportOut.toString(StandardCharsets.UTF_8); - assertTrue(exportText.contains("NEXT: follow AGENT-GUIDE Discover")); - assertTrue(exportText.contains("run-catalog.json")); + assertTrue(exportText.contains("NEXT: follow AGENT-GUIDE")); + assertTrue(exportText.contains("Workbench discover")); assertFalse(exportText.contains("mcp .")); assertTrue(Files.isRegularFile(root.resolve("AGENT-GUIDE.md"))); assertTrue(Files.isRegularFile(root.resolve("GUIDANCE-MANIFEST.json"))); @@ -280,17 +278,19 @@ void dependencyExportsVersionMatchedGuidanceAndManifest() throws Exception { assertTrue(guide.contains("pkb_reportretention=failed")); assertTrue(guide.contains("Do not copy, modify, or execute files")); assertTrue(guide.contains("tell the human")); - assertTrue(guide.contains("CLI/Maven-primary")); + assertTrue(guide.contains("one front door")); assertTrue(guide.contains("PickleballWorkbenchLauncher")); - assertTrue(guide.contains("Maintainer pointer-eval harness")); + assertTrue(guide.contains("Maintainer-only: pointer-eval harness")); assertTrue(guide.contains("@agent-pointer-eval")); String consumerProject = Files.readString(root.resolve("docs/consumer-project.md")); assertTrue(consumerProject.contains("keep console verbosity low")); assertTrue(consumerProject.contains("older Pickleball release whose exporter predates the manifest lifecycle")); assertTrue(consumerProject.contains("Version-matched reference snapshot")); - assertTrue(consumerProject.contains("discover which scenarios fail")); - assertTrue(consumerProject.contains("do not self-register IDE MCP")); + assertTrue(consumerProject.contains("discover which scenarios fail") + || consumerProject.contains("one front door")); + assertTrue(consumerProject.contains("do not self-register IDE MCP") + || consumerProject.contains("Do not register IDE MCP")); } finally { deleteTree(root); } diff --git a/maven-consumer-project/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandChecks.java b/maven-consumer-project/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandChecks.java new file mode 100644 index 00000000..8e6b2d0c --- /dev/null +++ b/maven-consumer-project/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandChecks.java @@ -0,0 +1,69 @@ +package tools.dscode.launcher; + +import org.junit.jupiter.api.Test; +import tools.dscode.common.reporting.diagnostic.AgentDiscoverPlanner; +import tools.dscode.testengine.PKB_props; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class WorkbenchAgentCommandChecks { + @Test + void launcherNormalizesAgentAndHostCommands() { + String[] ui = PickleballWorkbenchLauncher.normalizedArguments(new String[0]); + String[] mcp = PickleballWorkbenchLauncher.normalizedArguments(new String[]{"mcp"}); + String[] isolate = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"isolate", "--tags=@one"} + ); + String[] exportGuidance = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"export-guidance", ".pickleball"} + ); + + assertEquals("ui", ui[0]); + assertEquals("mcp", mcp[0]); + assertEquals("isolate", isolate[0]); + assertEquals("--tags", isolate[2]); + assertEquals("@one", isolate[3]); + assertEquals("export-guidance", exportGuidance[0]); + assertEquals(".pickleball", exportGuidance[1]); + + String[] isolateName = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"isolate", "--name=The", "failing", "scenario"} + ); + assertEquals("The failing scenario", isolateName[3]); + } + + @Test + void isolateReplayDoesNotReinheritOptionalDefaults() { + LinkedHashMap retained = new LinkedHashMap<>(); + retained.put(PKB_props.PKB_BROWSER, "CHROME_HEADLESS"); + retained.put(PKB_props.PKB_PARALLEL, "10"); + retained.put(PKB_props.PKB_GLUE, "com.example.pickleball"); + retained.put(PKB_props.PKB_REPORTING_MODE, "diagnostic"); + + String isolate = AgentDiscoverPlanner.isolateRunVars(retained, "@broken", null); + assertTrue(isolate.contains("pkb_browser=CHROME_HEADLESS")); + assertTrue(isolate.contains("pkb_parallel=1")); + assertTrue(isolate.contains("pkb_glue=com.example.pickleball")); + assertFalse(isolate.contains("pkb_run_profile=")); + assertFalse(isolate.contains("@all")); + assertFalse(isolate.contains("pretty")); + } + + @Test + void confirmReplaysSnapshot() { + Map retained = Map.of( + PKB_props.PKB_BROWSER, "GRID_CHROME", + PKB_props.PKB_PARALLEL, "6" + ); + String confirm = AgentDiscoverPlanner.confirmRunVars(retained, "@one", null); + assertTrue(confirm.contains("pkb_browser=GRID_CHROME")); + assertTrue(confirm.contains("pkb_parallel=6")); + assertTrue(confirm.contains("pkb_tags=@one")); + } +} diff --git a/pickleball-control-protocol/src/main/java/tools/dscode/control/protocol/ControlProtocol.java b/pickleball-control-protocol/src/main/java/tools/dscode/control/protocol/ControlProtocol.java index d26970d8..5ac27b44 100644 --- a/pickleball-control-protocol/src/main/java/tools/dscode/control/protocol/ControlProtocol.java +++ b/pickleball-control-protocol/src/main/java/tools/dscode/control/protocol/ControlProtocol.java @@ -12,6 +12,9 @@ public final class ControlProtocol { "pickleball.workbench.testOutputRoot"; public static final String EMBEDDED_WORKBENCH_RESOURCE = "META-INF/pickleball/workbench/pickleball-workbench.jar"; + /** Written by Workbench discover; isolate/confirm replay this snapshot as {@code pkb_runvars}. */ + public static final String LAST_DISCOVER_SNAPSHOT_RELATIVE = + ".pickleball/workbench/last-discover.json"; /* * Reserved neutral references used over the existing Mapping snapshot/restore diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchApplication.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchApplication.java index 11e56d32..708aeafd 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchApplication.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchApplication.java @@ -63,10 +63,16 @@ static int run(String[] args, PrintStream out, PrintStream err) { case "status" -> status(args, out); case "worker-check" -> workerCheck(args, out); case "live-check" -> liveCheck(args, out); + case "isolate" -> isolate(args, out, err); case "ui" -> ui(args); case "mcp" -> throw new IllegalArgumentException( "MCP mode must be launched through the Workbench executable." ); + case "export-guidance", "hint", "discover-hint", "discover", "confirm" -> { + err.println("Workbench " + args[0] + + " runs through PickleballWorkbenchLauncher in the consumer JVM, not this controller JAR."); + yield 2; + } default -> { err.println("Unknown Workbench command: " + args[0]); err.println("Run with --help for available commands."); @@ -177,6 +183,75 @@ private static int ui(String[] args) { return 0; } + private static int isolate(String[] args, PrintStream out, PrintStream err) { + IsolateArgs parsed = isolateArgs(args); + Map workerProperties; + try { + workerProperties = tools.dscode.workbench.discover.LastDiscoverSnapshot.workerSystemProperties( + parsed.project(), parsed.tags(), parsed.name() + ); + } catch (RuntimeException failure) { + err.println(failure.getMessage()); + err.println("Workbench CLI isolate failed. Do not register IDE MCP."); + return 1; + } + + try { + new WorkbenchSynchronizer().sync(parsed.project()); + try (WorkbenchLiveSession live = new WorkbenchLiveSession(parsed.project(), workerProperties)) { + WorkbenchWorkerStatus started = live.start(); + requireInteractiveWorker(started, "isolate"); + out.println("Workbench isolate worker: pid=" + started.pid() + + " scenario=" + started.scenarioId()); + out.println("Replayed pkb_runvars=" + workerProperties.get("pkb_runvars")); + out.println("Isolate stays one paused scenario. Do not start the GUI."); + if (Boolean.getBoolean("pickleball.workbench.isolate.once")) { + requireCleanStop(live.stop()); + return 0; + } + try { + System.in.read(); + } catch (IOException ignored) { + // stdin closed; stop the worker + } + requireCleanStop(live.stop()); + } + return 0; + } catch (RuntimeException failure) { + err.println("Workbench CLI isolate failed: " + failure.getMessage()); + err.println("Do not register IDE MCP."); + return 1; + } + } + + private record IsolateArgs(Path project, String tags, String name) { + } + + private static IsolateArgs isolateArgs(String[] args) { + if (args.length < 2 || args[1].isBlank() || args[1].startsWith("-")) { + throw new IllegalArgumentException( + "Usage: pickleball-workbench isolate [--tags ] [--name ]" + ); + } + String tags = null; + String name = null; + for (int index = 2; index < args.length; index++) { + String token = args[index]; + if (token.startsWith("--tags=")) { + tags = token.substring("--tags=".length()); + } else if ("--tags".equals(token) && index + 1 < args.length) { + tags = args[++index]; + } else if (token.startsWith("--name=")) { + name = token.substring("--name=".length()); + } else if ("--name".equals(token) && index + 1 < args.length) { + name = args[++index]; + } else if (name != null && !token.startsWith("-")) { + name = name + " " + token; + } + } + return new IsolateArgs(Path.of(args[1]), tags, name); + } + private static int workerCheck(String[] args, PrintStream out) { Path project = requiredProject(args, "worker-check"); try (WorkbenchWorkerManager workers = new WorkbenchWorkerManager(project)) { @@ -428,14 +503,17 @@ private static void printUsage(PrintStream out) { out.println(" java -jar pickleball-workbench-.jar status "); out.println(" java -jar pickleball-workbench-.jar worker-check "); out.println(" java -jar pickleball-workbench-.jar live-check "); + out.println(" java -jar pickleball-workbench-.jar isolate [--tags ] [--name ]"); out.println(" java -jar pickleball-workbench-.jar mcp "); out.println(" java -jar pickleball-workbench-.jar ui "); out.println(" java -jar pickleball-workbench-.jar --version"); out.println(); + out.println("Agent-facing Discover/hint/export-guidance/confirm run through PickleballWorkbenchLauncher."); out.println("sync uses the selected project wrapper and materializes .pickleball/workbench."); out.println("worker-check starts, restarts, and gracefully stops direct consumer workers without rebuilding."); out.println("live-check exercises raw Gherkin, Step Override, and live runtime operations on one persistent worker."); - out.println("mcp serves the same Workbench services over protocol-only stdio; diagnostics use stderr/log files."); + out.println("isolate starts/reuses a live worker from the last Discover snapshot replayed as pkb_runvars. Do not start ui for agents."); + out.println("mcp serves the same Workbench services over protocol-only stdio; optional host wiring, not an agent setup step."); out.println("ui opens the thin Swing Workbench over the same controller services and writes a localhost agent-attach endpoint to .pickleball/workbench/attach.json."); } } diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/discover/LastDiscoverSnapshot.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/discover/LastDiscoverSnapshot.java new file mode 100644 index 00000000..9be6875c --- /dev/null +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/discover/LastDiscoverSnapshot.java @@ -0,0 +1,142 @@ +package tools.dscode.workbench.discover; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import tools.dscode.control.protocol.ControlProtocol; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * Reads the last Workbench Discover snapshot and replays it as compact + * {@code pkb_runvars} for isolate/worker start. + */ +public final class LastDiscoverSnapshot { + private static final ObjectMapper JSON = new ObjectMapper(); + public static final String MISSING_SNAPSHOT_MESSAGE = + "Workbench CLI isolate failed: no prior Discover snapshot. " + + "Run Workbench discover first. Isolate will not silently re-resolve from project defaults."; + + private LastDiscoverSnapshot() { + } + + public record Snapshot(String runId, String runProfile, Map runVars) { + public Snapshot { + runVars = runVars == null ? Map.of() : Map.copyOf(runVars); + } + + public boolean present() { + return (runVars != null && !runVars.isEmpty()) + || (runProfile != null && !runProfile.isBlank()); + } + } + + public static Path file(Path projectRoot) { + return projectRoot.toAbsolutePath().normalize().resolve(ControlProtocol.LAST_DISCOVER_SNAPSHOT_RELATIVE); + } + + public static Snapshot read(Path projectRoot) { + Path file = file(projectRoot); + if (!Files.isRegularFile(file)) return null; + try { + JsonNode root = JSON.readTree(file.toFile()); + LinkedHashMap runVars = new LinkedHashMap<>(); + JsonNode vars = root.get("runVars"); + if (vars != null && vars.isObject()) { + vars.fields().forEachRemaining(entry -> + runVars.put(entry.getKey(), entry.getValue().isNull() ? "" : entry.getValue().asText()) + ); + } + return new Snapshot( + text(root, "runId"), + text(root, "runProfile"), + runVars + ); + } catch (IOException failure) { + throw new IllegalStateException("Could not read Discover snapshot: " + file, failure); + } + } + + public static Snapshot require(Path projectRoot) { + Snapshot snapshot = read(projectRoot); + if (snapshot == null || !snapshot.present()) { + throw new IllegalStateException(MISSING_SNAPSHOT_MESSAGE); + } + return snapshot; + } + + public static Map workerSystemProperties(Path projectRoot, String tags, String name) { + return Map.of("pkb_runvars", replay(require(projectRoot), true, tags, name)); + } + + public static Map workerSystemPropertiesIfPresent(Path projectRoot) { + Snapshot snapshot = read(projectRoot); + if (snapshot == null || !snapshot.present()) return Map.of(); + return Map.of("pkb_runvars", replay(snapshot, true, null, null)); + } + + public static String replay(Snapshot snapshot, boolean isolate, String tags, String name) { + Map values = new TreeMap<>(); + if (snapshot.runVars() != null) values.putAll(snapshot.runVars()); + if (values.isEmpty() && snapshot.runProfile() != null && !snapshot.runProfile().isBlank()) { + values.putAll(parseCompact(snapshot.runProfile())); + } + values.remove("pkb_run_profile"); + if (isolate) values.put("pkb_parallel", "1"); + if (tags != null && !tags.isBlank()) values.put("pkb_tags", tags.trim()); + if (name != null && !name.isBlank()) values.put("pkb_name", name.trim()); + return serializeCompact(values); + } + + static Map parseCompact(String compact) { + LinkedHashMap values = new LinkedHashMap<>(); + if (compact == null || compact.isBlank()) return values; + for (String assignment : compact.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)", -1)) { + String item = assignment.trim(); + int equals = item.indexOf('='); + if (equals <= 0) continue; + String key = item.substring(0, equals).trim(); + String value = unquote(item.substring(equals + 1).trim()); + if (!key.isBlank() && !"pkb_run_profile".equals(key)) values.put(key, value); + } + return values; + } + + static String serializeCompact(Map values) { + StringBuilder out = new StringBuilder(); + values.entrySet().stream() + .filter(entry -> entry.getKey() != null && !entry.getKey().isBlank()) + .filter(entry -> !"pkb_run_profile".equals(entry.getKey())) + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> { + if (!out.isEmpty()) out.append(", "); + out.append(entry.getKey()).append('=').append(quoteIfNeeded(entry.getValue())); + }); + return out.toString(); + } + + private static String text(JsonNode root, String field) { + JsonNode node = root.get(field); + return node == null || node.isNull() ? "" : node.asText(""); + } + + private static String unquote(String value) { + if (value.length() < 2) return value == null ? "" : value; + char quote = value.charAt(0); + if ((quote != '"' && quote != '\'') || value.charAt(value.length() - 1) != quote) return value; + return value.substring(1, value.length() - 1).replace("\\\"", "\"").replace("\\\\", "\\"); + } + + private static String quoteIfNeeded(String value) { + if (value == null) return ""; + if (value.indexOf(',') >= 0 || value.indexOf(';') >= 0 || value.indexOf('"') >= 0 + || !value.equals(value.trim())) { + return '"' + value.replace("\\", "\\\\").replace("\"", "\\\"") + '"'; + } + return value; + } +} diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/worker/WorkbenchWorkerManager.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/worker/WorkbenchWorkerManager.java index bd53a710..4bdfa7f7 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/worker/WorkbenchWorkerManager.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/worker/WorkbenchWorkerManager.java @@ -70,8 +70,11 @@ public synchronized WorkbenchWorkerStatus startInteractive() { createDirectories(sessionDirectory, stdout.getParent()); writeAnchor(anchor); + Map properties = workerSystemProperties.isEmpty() + ? tools.dscode.workbench.discover.LastDiscoverSnapshot.workerSystemPropertiesIfPresent(projectRoot) + : workerSystemProperties; ProcessBuilder builder = new ProcessBuilder(workerCommand( - manifest, classpath, anchor, workerSystemProperties + manifest, classpath, anchor, properties )) .directory(projectRoot.toFile()) .redirectOutput(stdout.toFile()) diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java index 7795b4e9..ded82106 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java @@ -5,8 +5,10 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class WorkbenchApplicationTest { @@ -22,6 +24,8 @@ void helpListsSynchronizationWorkerLiveMcpAndUiCommands() { assertTrue(output.stdout().contains("live-check ")); assertTrue(output.stdout().contains("mcp ")); assertTrue(output.stdout().contains("ui ")); + assertTrue(output.stdout().contains("isolate ")); + assertTrue(output.stdout().contains("PickleballWorkbenchLauncher")); assertEquals("", output.stderr()); } @@ -71,6 +75,26 @@ void versionAliasesPrintTheSameVersion() { assertEquals("", word.stderr()); } + @Test + void isolateWithoutSnapshotFailsClearlyWithoutSuggestingIdeMcp() { + Output output = run("isolate", Path.of("").toAbsolutePath().normalize().toString()); + + assertEquals(1, output.exitCode()); + assertTrue(output.stderr().contains("Workbench CLI isolate failed")); + assertTrue(output.stderr().contains("no prior Discover snapshot") + || output.stderr().contains("No prior Discover snapshot")); + assertFalse(output.stderr().toLowerCase().contains("register mcp")); + assertFalse(output.stderr().toLowerCase().contains("intellij")); + } + + @Test + void isolateRequiresAProject() { + Output output = run("isolate"); + + assertEquals(1, output.exitCode()); + assertTrue(output.stderr().contains("Usage: pickleball-workbench isolate ")); + } + @Test void unknownCommandsFailClearly() { Output output = run("unknown"); diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/discover/LastDiscoverSnapshotTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/discover/LastDiscoverSnapshotTest.java new file mode 100644 index 00000000..36f0c097 --- /dev/null +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/discover/LastDiscoverSnapshotTest.java @@ -0,0 +1,64 @@ +package tools.dscode.workbench.discover; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LastDiscoverSnapshotTest { + @TempDir + Path tempDir; + + @Test + void missingSnapshotFailsWithoutSuggestingIdeMcp() { + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> LastDiscoverSnapshot.require(tempDir) + ); + assertTrue(failure.getMessage().contains("Workbench CLI isolate failed")); + assertFalse(failure.getMessage().toLowerCase().contains("register")); + } + + @Test + void isolateReplayUsesRetainedRunVarsAndForcesParallelOne() throws Exception { + Path file = LastDiscoverSnapshot.file(tempDir); + Files.createDirectories(file.getParent()); + Files.writeString(file, """ + { + "schemaVersion": 1, + "source": "workbench-discover", + "runId": "run-1", + "runProfile": "pkb_browser=CHROME_HEADLESS, pkb_glue=com.example, pkb_parallel=12, pkb_plugins=, pkb_reportingmode=diagnostic", + "runVars": { + "pkb_browser": "CHROME_HEADLESS", + "pkb_glue": "com.example", + "pkb_parallel": "12", + "pkb_plugins": "", + "pkb_reportingmode": "diagnostic" + } + } + """); + + Map properties = LastDiscoverSnapshot.workerSystemProperties( + tempDir, "@broken", "Failing scenario" + ); + + assertEquals(1, properties.size()); + String runVars = properties.get("pkb_runvars"); + assertTrue(runVars.contains("pkb_browser=CHROME_HEADLESS")); + assertTrue(runVars.contains("pkb_glue=com.example")); + assertTrue(runVars.contains("pkb_parallel=1")); + assertTrue(runVars.contains("pkb_tags=@broken")); + assertFalse(runVars.contains("pkb_parallel=12")); + assertFalse(runVars.contains("pkb_run_profile=")); + assertFalse(runVars.contains("pretty")); + assertFalse(runVars.contains("@all")); + } +} diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/worker/WorkbenchWorkerManagerTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/worker/WorkbenchWorkerManagerTest.java index 90ccf940..2fe6c174 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/worker/WorkbenchWorkerManagerTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/worker/WorkbenchWorkerManagerTest.java @@ -77,6 +77,26 @@ void workerSystemPropertiesAreExplicitAndDeterministic() { assertTrue(command.indexOf("-Dpkb_tags=@smoke") < command.indexOf("-cp")); } + @Test + void isolateReplayUsesCompactPkbRunvarsNotRunProfileInput() { + Path project = tempDir.resolve("consumer").toAbsolutePath().normalize(); + Path live = project.resolve(".pickleball/workbench/live/classes"); + Path dependency = tempDir.resolve("pickleball.jar").toAbsolutePath().normalize(); + Path anchor = project.resolve(".pickleball/workbench/sessions/test/anchor.feature"); + String runVars = "pkb_browser=CHROME_HEADLESS, pkb_glue=com.example, pkb_parallel=1"; + + List command = WorkbenchWorkerManager.workerCommand( + manifest(project, live, dependency), + List.of(live.toString(), dependency.toString()), + anchor, + Map.of("pkb_runvars", runVars) + ); + + assertTrue(command.contains("-Dpkb_runvars=" + runVars)); + assertFalse(command.stream().anyMatch(item -> item.startsWith("-Dpkb_run_profile="))); + assertFalse(command.stream().anyMatch(item -> item.equals("-Dpkb_browser=chrome"))); + } + @Test void interactiveWorkerUsesSafeAnchorStepPauseBoundary() { assertEquals("BEFORE_STEP", WorkbenchWorkerManager.INTERACTIVE_PAUSE_HOOK); diff --git a/scripts/verify_agent_contract.py b/scripts/verify_agent_contract.py index cd933bb1..fcd5897c 100644 --- a/scripts/verify_agent_contract.py +++ b/scripts/verify_agent_contract.py @@ -297,48 +297,63 @@ def validate_consumer_bridge(errors: list[str]) -> None: nonblank_lines = [line for line in text.splitlines() if line.strip()] if not (3 <= len(nonblank_lines) <= 8): errors.append( - "Consumer guidance bridge must keep the export-guidance one-liner plus a short " - "discover-vs-isolate pointer (not the full guide): " + "Consumer guidance bridge must keep the Workbench export-guidance one-liner plus a short " + "Discover/Isolate/Confirm pointer (not the full guide): " + relative ) first_line = nonblank_lines[0] if nonblank_lines else "" for required in ( - "DiagnosticCli", + "PickleballWorkbenchLauncher", "export-guidance", ".pickleball/AGENT-GUIDE.md", + "classpathScope=test", ): if required not in first_line: errors.append( f"Consumer guidance bridge one-liner must reference {required}: {relative}" ) - if "mcp ." not in text and "workbench_" not in text: + lowered = text.lower() + if "do not start the gui" not in lowered: errors.append( - "Consumer guidance bridge must mention MCP (`mcp .`) or Workbench tools " - "(`workbench_`): " + relative + "Consumer guidance bridge must say not to start the GUI: " + relative + ) + if "do not register ide mcp" not in lowered: + errors.append( + "Consumer guidance bridge must say not to register IDE MCP: " + relative ) for required in ( - "mvn test", - "diagnostic", + "discover", + "isolate", + "confirm", + "workbench_", ): - if required not in text: + if required not in lowered: errors.append( - f"Consumer guidance bridge must state the discover-vs-isolate split ({required}): " - + relative + f"Consumer guidance bridge must mention {required}: " + relative ) - lowered = text.lower() - if "do not skip workbench" not in lowered: + if "do not skip workbench" in lowered: errors.append( - "Consumer guidance bridge must say not to skip Workbench: " + relative + "Consumer guidance bridge must not say 'Do not skip Workbench': " + relative ) - if "do not start the gui" not in lowered: + if "parallel is ok when the project supports it" in lowered: errors.append( - "Consumer guidance bridge must say not to start the GUI: " + relative + "Consumer guidance bridge must not say parallel is optional: " + relative + ) + if "stdio" in lowered: + errors.append( + "Consumer guidance bridge must not mention IDE-owned stdio: " + relative + ) + if "must" in lowered and "chrome_headless" in lowered: + errors.append( + "Consumer guidance bridge must not MUST-use CHROME_HEADLESS for all projects: " + + relative ) for forbidden in ( + "DiagnosticCli", "GUIDANCE-MANIFEST.json", ".git/info/exclude", "pkb_changed_variables", @@ -347,11 +362,12 @@ def validate_consumer_bridge(errors: list[str]) -> None: "attach.json", "ui .", "@agent-pointer-eval", + "IntelliJ", ): if forbidden in text: errors.append( f"Consumer guidance bridge contains dependency-owned guidance ({forbidden}); " - "keep only the bootstrap command and a short discover-vs-isolate pointer: " + "keep only the Workbench bootstrap command and a short Discover/Isolate/Confirm pointer: " + relative ) diff --git a/src/main/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadder.java b/src/main/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadder.java new file mode 100644 index 00000000..e3996ddf --- /dev/null +++ b/src/main/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadder.java @@ -0,0 +1,114 @@ +package tools.dscode.common.reporting.diagnostic; + +import tools.dscode.testengine.PKB_props; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + * Discover/Confirm browser selection for consumer AI agents. + * + *

Remote farm browsers already configured as the project's {@code pkb_browser} + * are kept. Local or missing browsers prefer {@code CHROME_HEADLESS}. Unused + * SAUCE/GRID/REMOTE yaml files under {@code configs/} are never auto-selected.

+ */ +public final class AgentBrowserLadder { + public static final String CHROME_HEADLESS = "CHROME_HEADLESS"; + + public record Decision( + String browser, + String projectBrowser, + String reason, + boolean remoteKept + ) { + } + + private AgentBrowserLadder() { + } + + public static Decision select(Path projectRoot) { + String projectBrowser = readProjectBrowser(projectRoot); + if (isRemoteBrowser(projectBrowser)) { + return new Decision( + projectBrowser, + projectBrowser, + "project pkb_browser is a remote farm name; keep it", + true + ); + } + return new Decision( + CHROME_HEADLESS, + projectBrowser == null ? "" : projectBrowser, + "local or unset pkb_browser prefers CHROME_HEADLESS", + false + ); + } + + /** + * Retry browser only when local headless cannot start and the project's + * configured {@code pkb_browser} is already a remote farm name. + */ + public static String fallbackIfHeadlessCannotStart(Decision decision) { + if (decision == null || decision.remoteKept()) return null; + return isRemoteBrowser(decision.projectBrowser()) ? decision.projectBrowser() : null; + } + + public static boolean isRemoteBrowser(String browser) { + if (browser == null || browser.isBlank()) return false; + String name = browser.trim().toUpperCase(Locale.ROOT); + return name.startsWith("SAUCE") + || name.startsWith("GRID") + || name.startsWith("REMOTE"); + } + + public static boolean looksLikeLocalBrowserFailure(String output) { + if (output == null || output.isBlank()) return false; + String text = output.toLowerCase(Locale.ROOT); + return text.contains("chromedriver") + || text.contains("cannot find chrome") + || text.contains("chrome binary") + || text.contains("session not created") + || text.contains("devtoolsactiveport") + || text.contains("chrome not reachable") + || text.contains("unable to establish") + || text.contains("webdriver"); + } + + static String readProjectBrowser(Path projectRoot) { + if (projectRoot == null) return ""; + Path project = projectRoot.toAbsolutePath().normalize(); + for (Path file : propertyFiles(project)) { + if (!Files.isRegularFile(file)) continue; + String browser = readBrowserProperty(file); + if (browser != null && !browser.isBlank()) return browser.trim(); + } + return ""; + } + + private static List propertyFiles(Path project) { + return List.of( + project.resolve("src/test/resources/pickleball_local.properties"), + project.resolve("src/test/resources/pickleball.properties"), + project.resolve("src/main/resources/pickleball_local.properties"), + project.resolve("src/main/resources/pickleball.properties"), + project.resolve("pickleball_local.properties"), + project.resolve("pickleball.properties") + ); + } + + private static String readBrowserProperty(Path file) { + Properties properties = new Properties(); + try (var reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) { + properties.load(reader); + } catch (IOException ignored) { + return ""; + } + String value = properties.getProperty(PKB_props.PKB_BROWSER); + return value == null ? "" : value.trim(); + } +} diff --git a/src/main/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlanner.java b/src/main/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlanner.java new file mode 100644 index 00000000..729865bf --- /dev/null +++ b/src/main/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlanner.java @@ -0,0 +1,85 @@ +package tools.dscode.common.reporting.diagnostic; + +import tools.dscode.parallelutilities.ParallelCountEstimator; +import tools.dscode.testengine.PKB_props; + +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +/** Builds the complete AI Discover/Confirm {@code pkb_runvars} set. */ +public final class AgentDiscoverPlanner { + private AgentDiscoverPlanner() { + } + + public record Plan( + Path projectRoot, + AgentBrowserLadder.Decision browser, + String runVars, + String tags, + String name + ) { + } + + public static Plan discover(Path projectRoot, String tags, String name) { + AgentBrowserLadder.Decision browser = AgentBrowserLadder.select(projectRoot); + LinkedHashMap values = baseDiscoverVars(browser.browser()); + overlaySelection(values, tags, name); + return new Plan(projectRoot, browser, PKB_props.serializeRunVars(values), blankToNull(tags), blankToNull(name)); + } + + public static String isolateRunVars(Map retainedProfile, String tags, String name) { + if (retainedProfile == null || retainedProfile.isEmpty()) { + throw new IllegalStateException(missingDiscoverSnapshotMessage()); + } + LinkedHashMap values = copyRunVars(retainedProfile); + values.put(PKB_props.PKB_PARALLEL, "1"); + overlaySelection(values, tags, name); + return PKB_props.serializeRunVars(values); + } + + public static String confirmRunVars(Map retainedProfile, String tags, String name) { + if (retainedProfile == null || retainedProfile.isEmpty()) { + throw new IllegalStateException(missingDiscoverSnapshotMessage()); + } + LinkedHashMap values = copyRunVars(retainedProfile); + overlaySelection(values, tags, name); + return PKB_props.serializeRunVars(values); + } + + public static String missingDiscoverSnapshotMessage() { + return "No prior Discover snapshot. Run Workbench discover first. " + + "Isolate/Confirm will not silently re-resolve from project defaults."; + } + + private static LinkedHashMap baseDiscoverVars(String browser) { + LinkedHashMap values = new LinkedHashMap<>(); + values.put(PKB_props.PKB_BROWSER, browser); + values.put(PKB_props.PKB_PARALLEL, Integer.toString(ParallelCountEstimator.estimate())); + values.put(PKB_props.PKB_REPORTING_MODE, "diagnostic"); + values.put(PKB_props.PKB_LOGLEVEL, "warn"); + values.put(PKB_props.PKB_REPORT_RETENTION, "failed"); + return values; + } + + private static void overlaySelection(LinkedHashMap values, String tags, String name) { + if (tags != null && !tags.isBlank()) values.put(PKB_props.PKB_TAGS, tags.trim()); + if (name != null && !name.isBlank()) values.put(PKB_props.PKB_NAME, name.trim()); + values.remove(PKB_props.PKB_RUN_PROFILE); + } + + private static LinkedHashMap copyRunVars(Map retainedProfile) { + LinkedHashMap values = new LinkedHashMap<>(); + retainedProfile.forEach((key, value) -> { + if (key == null || key.isBlank()) return; + if (PKB_props.PKB_RUN_PROFILE.equals(key) || PKB_props.isRunProfileMemberKey(key)) return; + if (PKB_props.isRunMetadataKey(key)) return; + values.put(key, value == null ? "" : value); + }); + return values; + } + + private static String blankToNull(String value) { + return value == null || value.isBlank() ? null : value.trim(); + } +} diff --git a/src/main/java/tools/dscode/common/reporting/diagnostic/ConsumerMavenTestRunner.java b/src/main/java/tools/dscode/common/reporting/diagnostic/ConsumerMavenTestRunner.java new file mode 100644 index 00000000..da3d9b03 --- /dev/null +++ b/src/main/java/tools/dscode/common/reporting/diagnostic/ConsumerMavenTestRunner.java @@ -0,0 +1,86 @@ +package tools.dscode.common.reporting.diagnostic; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** Invokes the consumer Maven wrapper the same way {@code mvn test} would. */ +public final class ConsumerMavenTestRunner { + private ConsumerMavenTestRunner() { + } + + public interface ProcessLauncher { + int run(List command, Path directory, PrintStream out, PrintStream err) + throws IOException, InterruptedException; + } + + public static List command(Path projectRoot, String compactRunVars) { + List command = new ArrayList<>(); + command.add(wrapper(projectRoot).toString()); + command.add("test"); + command.add("-Dpkb_runvars=" + compactRunVars); + command.add("-Dpkb_run_purpose=workbench-discover"); + return List.copyOf(command); + } + + public static List confirmCommand(Path projectRoot, String compactRunVars) { + List command = new ArrayList<>(); + command.add(wrapper(projectRoot).toString()); + command.add("test"); + command.add("-Dpkb_runvars=" + compactRunVars); + command.add("-Dpkb_run_purpose=workbench-confirm"); + return List.copyOf(command); + } + + public static int run( + Path projectRoot, + List command, + PrintStream out, + PrintStream err + ) { + return run(projectRoot, command, out, err, inheritIoLauncher()); + } + + public static int run( + Path projectRoot, + List command, + PrintStream out, + PrintStream err, + ProcessLauncher launcher + ) { + try { + return launcher.run(command, projectRoot.toAbsolutePath().normalize(), out, err); + } catch (InterruptedException failure) { + Thread.currentThread().interrupt(); + err.println("Workbench Maven test was interrupted."); + return 1; + } catch (IOException failure) { + err.println("Workbench could not run Maven test: " + failure.getMessage()); + return 1; + } + } + + static Path wrapper(Path projectRoot) { + Path project = projectRoot.toAbsolutePath().normalize(); + boolean windows = System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win"); + Path script = project.resolve(windows ? "mvnw.cmd" : "mvnw"); + if (Files.isRegularFile(script)) return script; + Path alternate = project.resolve(windows ? "mvnw" : "mvnw.cmd"); + if (Files.isRegularFile(alternate)) return alternate; + return Path.of(windows ? "mvn.cmd" : "mvn"); + } + + private static ProcessLauncher inheritIoLauncher() { + return (command, directory, out, err) -> { + ProcessBuilder builder = new ProcessBuilder(command) + .directory(directory.toFile()) + .inheritIO(); + Process process = builder.start(); + return process.waitFor(); + }; + } +} diff --git a/src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java b/src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java index 734bbc55..49d8f7cd 100644 --- a/src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java +++ b/src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import tools.dscode.control.protocol.InvestigationHandoff; -import tools.dscode.parallelutilities.ParallelCountEstimator; import java.io.BufferedReader; import java.io.IOException; @@ -61,7 +60,7 @@ public static int run(String[] args, PrintStream out, PrintStream err, InputStre return switch (args[0]) { case "guidance" -> guidance(args, out); case "export-guidance" -> exportGuidance(args, out, err); - case "discover-hint" -> discoverHint(args, out); + case "discover-hint", "hint" -> discoverHint(args, out); case "emit-investigation" -> emitInvestigation(args, out, in); case "compare-runs" -> compareRuns(args, out); case "compare-fingerprints" -> compareFingerprints(args, out); @@ -127,21 +126,25 @@ private static int exportGuidance(String[] args, PrintStream out, PrintStream er out.println("Pickleball version: " + version); out.println("Manifest: " + root.resolve(GUIDANCE_MANIFEST)); out.println("Read " + root.resolve(AGENT_GUIDE)); - out.println("NEXT: follow AGENT-GUIDE Discover — run a diagnostic mvn test, then open run-catalog.json"); + out.println("NEXT: follow AGENT-GUIDE — run Workbench discover"); return 0; } private static int discoverHint(String[] args, PrintStream out) { - requireLength(args, 1, 1, "discover-hint"); - String runVars = ParallelCountEstimator.recommendedDiscoverRunVars(); - out.println("Recommended complete diagnostic Discover `pkb_runvars` (set these explicitly so headed Chrome / pretty / @all project defaults do not sneak in):"); - out.println("mvn test -Dpkb_runvars=\"" + runVars + "\""); + requireLength(args, 1, 2, "discover-hint [project]"); + Path project = args.length == 2 + ? Path.of(args[1]).toAbsolutePath().normalize() + : Path.of("").toAbsolutePath().normalize(); + AgentDiscoverPlanner.Plan plan = AgentDiscoverPlanner.discover(project, null, null); + out.println("Recommended complete diagnostic Discover `pkb_runvars` (Workbench honors the project browser ladder; headed Chrome / pretty / @all project defaults do not sneak in):"); + out.println("pkb_runvars=" + plan.runVars()); out.println(); - out.println("Multi-scenario Discover/Confirm must use CHROME_HEADLESS and this high pkb_parallel. Isolate / live Workbench stays one paused scenario — do not parallelize isolate. Headed Chrome only when the scenario actually needs a window."); - out.println("Execution-context keys (glue/features/data/call/component/configpath) may still inherit. After the run, read pkb_run_profile from run-catalog.json / run-index.json / summary.json for the complete resolved list, including inherited paths and the integer pkb_parallel. Do not assume omitted keys equal project pickleball.properties. Never supply pkb_run_profile as input."); + out.println("Browser: " + plan.browser().browser() + " (" + plan.browser().reason() + ")."); + out.println("Multi-scenario Discover/Confirm use this high pkb_parallel. Isolate stays one paused scenario — do not parallelize isolate."); + out.println("The agent-facing entry is Pickleball Workbench (`hint` / `discover` / `isolate` / `confirm`), not a separate DiagnosticCli story. After Discover, isolate and confirm replay the retained pkb_run_profile through pkb_runvars. Never supply pkb_run_profile as input."); out.println(); - out.println("After the run, read reports/diagnostic-runs/run-catalog.json, then only the relevant run-index.json / summary.json."); - out.println("NEXT: run the diagnostic mvn test above, then open run-catalog.json"); + out.println("After Discover, read reports/diagnostic-runs/run-catalog.json, then only the relevant run-index.json / summary.json."); + out.println("NEXT: run discover"); return 0; } @@ -413,7 +416,7 @@ private static void writeExportedAgentGuide(Path target, String version) throws > **Version-matched generated guidance** - > Exported from Pickleball `%s` by `DiagnosticCli export-guidance`. + > Exported from Pickleball `%s` by Workbench `export-guidance`. > Before relying on `.pickleball`, rerun the consumer bridge export command so this directory matches the currently resolved Pickleball Maven dependency. > If export fails, treat any existing `.pickleball` contents as potentially stale. See `GUIDANCE-MANIFEST.json` for the completed export's version and managed-file list. @@ -429,7 +432,7 @@ private static void writeGuidanceManifest(Path root, String version, List "); out.println(" DiagnosticCli compare-runs [output-json]"); out.println(" DiagnosticCli compare-fingerprints [output-json]"); out.println(" DiagnosticCli rebuild "); + out.println("Agents should run export-guidance, hint, discover, isolate, and confirm through PickleballWorkbenchLauncher."); } private record GitLayout(Path repositoryRoot, Path gitDirectory, Path commonGitDirectory) { diff --git a/src/main/java/tools/dscode/common/reporting/diagnostic/LastDiscoverSnapshot.java b/src/main/java/tools/dscode/common/reporting/diagnostic/LastDiscoverSnapshot.java new file mode 100644 index 00000000..e6273701 --- /dev/null +++ b/src/main/java/tools/dscode/common/reporting/diagnostic/LastDiscoverSnapshot.java @@ -0,0 +1,191 @@ +package tools.dscode.common.reporting.diagnostic; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import tools.dscode.control.protocol.ControlProtocol; +import tools.dscode.testengine.PKB_props; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** Reads and writes the last Workbench Discover snapshot for isolate/confirm replay. */ +public final class LastDiscoverSnapshot { + private static final ObjectMapper JSON = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + public static final String SOURCE = "workbench-discover"; + + private LastDiscoverSnapshot() { + } + + public record Snapshot( + int schemaVersion, + String source, + String runId, + String catalogPath, + String runProfile, + Map runVars, + String createdAt + ) { + public Snapshot { + runVars = runVars == null ? Map.of() : Map.copyOf(runVars); + } + + public boolean hasRunVars() { + return runVars != null && !runVars.isEmpty(); + } + } + + public static Path file(Path projectRoot) { + return projectRoot.toAbsolutePath().normalize().resolve(ControlProtocol.LAST_DISCOVER_SNAPSHOT_RELATIVE); + } + + public static Snapshot read(Path projectRoot) { + Path file = file(projectRoot); + if (!Files.isRegularFile(file)) return null; + try { + @SuppressWarnings("unchecked") + Map raw = JSON.readValue(file.toFile(), LinkedHashMap.class); + return fromMap(raw); + } catch (IOException failure) { + throw new IllegalStateException("Could not read Discover snapshot: " + file, failure); + } + } + + public static Snapshot require(Path projectRoot) { + Snapshot snapshot = read(projectRoot); + if (snapshot == null || !snapshot.hasRunVars() && isBlank(snapshot.runProfile())) { + throw new IllegalStateException(AgentDiscoverPlanner.missingDiscoverSnapshotMessage()); + } + return snapshot; + } + + public static Map retainedRunVars(Snapshot snapshot) { + if (snapshot == null) { + throw new IllegalStateException(AgentDiscoverPlanner.missingDiscoverSnapshotMessage()); + } + if (snapshot.hasRunVars()) return new LinkedHashMap<>(snapshot.runVars()); + if (!isBlank(snapshot.runProfile())) { + return PKB_props.parseAssignments(snapshot.runProfile()); + } + throw new IllegalStateException(AgentDiscoverPlanner.missingDiscoverSnapshotMessage()); + } + + public static Snapshot write( + Path projectRoot, + String runId, + Path catalogPath, + String runProfile + ) throws IOException { + LinkedHashMap runVars = isBlank(runProfile) + ? new LinkedHashMap<>() + : PKB_props.parseAssignments(runProfile); + Snapshot snapshot = new Snapshot( + 1, + SOURCE, + runId == null ? "" : runId, + catalogPath == null ? "" : catalogPath.toString(), + runProfile == null ? "" : runProfile, + runVars, + Instant.now().toString() + ); + Path file = file(projectRoot); + if (file.getParent() != null) Files.createDirectories(file.getParent()); + JSON.writeValue(file.toFile(), snapshotToMap(snapshot)); + return snapshot; + } + + public static CatalogRun latestCatalogRun(Path projectRoot) throws IOException { + return latestCatalogRun(projectRoot, null); + } + + public static CatalogRun latestCatalogRun(Path projectRoot, String purpose) throws IOException { + Path catalog = projectRoot.toAbsolutePath().normalize() + .resolve("reports") + .resolve("diagnostic-runs") + .resolve("run-catalog.json"); + if (!Files.isRegularFile(catalog)) return null; + @SuppressWarnings("unchecked") + Map raw = JSON.readValue(catalog.toFile(), LinkedHashMap.class); + Object runsValue = raw.get("runs"); + if (!(runsValue instanceof List runs) || runs.isEmpty()) return null; + CatalogRun matchedPurpose = null; + CatalogRun first = null; + for (Object item : runs) { + if (!(item instanceof Map run)) continue; + CatalogRun candidate = new CatalogRun( + catalog, + text(run.get("runId")), + text(run.get("runProfile")), + text(run.get("outcome")), + lineagePurpose(run.get("lineage")) + ); + if (first == null) first = candidate; + if (purpose != null && purpose.equals(candidate.purpose()) && matchedPurpose == null) { + matchedPurpose = candidate; + } + } + if (matchedPurpose != null) return matchedPurpose; + return purpose == null ? first : null; + } + + public record CatalogRun(Path catalog, String runId, String runProfile, String outcome, String purpose) { + public CatalogRun(Path catalog, String runId, String runProfile, String outcome) { + this(catalog, runId, runProfile, outcome, ""); + } + } + + private static String lineagePurpose(Object lineage) { + if (lineage instanceof Map map) { + Object purpose = map.get("runPurpose"); + if (purpose == null) purpose = map.get("pkb_run_purpose"); + return text(purpose); + } + return ""; + } + + private static Snapshot fromMap(Map raw) { + LinkedHashMap runVars = new LinkedHashMap<>(); + Object vars = raw.get("runVars"); + if (vars instanceof Map map) { + map.forEach((key, value) -> { + if (key != null) runVars.put(String.valueOf(key), value == null ? "" : String.valueOf(value)); + }); + } + int schema = 1; + Object schemaValue = raw.get("schemaVersion"); + if (schemaValue instanceof Number number) schema = number.intValue(); + return new Snapshot( + schema, + text(raw.get("source")), + text(raw.get("runId")), + text(raw.get("catalogPath")), + text(raw.get("runProfile")), + runVars, + text(raw.get("createdAt")) + ); + } + + private static Map snapshotToMap(Snapshot snapshot) { + Map body = new LinkedHashMap<>(); + body.put("schemaVersion", snapshot.schemaVersion()); + body.put("source", snapshot.source()); + body.put("runId", snapshot.runId()); + body.put("catalogPath", snapshot.catalogPath()); + body.put("runProfile", snapshot.runProfile()); + body.put("runVars", snapshot.runVars()); + body.put("createdAt", snapshot.createdAt()); + return body; + } + + private static String text(Object value) { + return value == null ? "" : String.valueOf(value); + } + + private static boolean isBlank(String value) { + return value == null || value.isBlank(); + } +} diff --git a/src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java b/src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java index b4703c34..b65e0469 100644 --- a/src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java +++ b/src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java @@ -16,11 +16,14 @@ import java.util.ArrayList; import java.util.HexFormat; import java.util.List; -import java.util.Set; /** * Extracts the version-matched, controller-only Workbench payload from the * consumer's Pickleball dependency and always launches it in a separate JVM. + * + *

Agent-facing Discover/hint/export-guidance/confirm run in this consumer JVM + * so they can reuse DiagnosticCli and wrap Maven. Isolate, UI, MCP, and sync + * still extract and forward to the controller JAR.

*/ public final class PickleballWorkbenchLauncher { /** @@ -29,16 +32,20 @@ public final class PickleballWorkbenchLauncher { * limit rather than an in-memory buffer size. */ static final long MAX_PAYLOAD_BYTES = 512L * 1024 * 1024; - private static final Set PROJECT_COMMANDS = Set.of( - "sync", "status", "worker-check", "live-check", "ui", "mcp" - ); private PickleballWorkbenchLauncher() { } public static void main(String[] args) { - String[] forwarded = normalizedArguments(args); - Path project = projectRoot(forwarded); + WorkbenchCommandLine.Parsed parsed = WorkbenchCommandLine.parse(args); + if (WorkbenchCommandLine.isAgentCoreCommand(parsed.command())) { + int exitCode = WorkbenchAgentCommands.run(args, System.out, System.err); + if (exitCode != 0) System.exit(exitCode); + return; + } + + String[] forwarded = parsed.forwarded() != null ? parsed.forwarded() : normalizedArguments(args); + Path project = parsed.project() != null ? parsed.project() : projectRoot(forwarded); try { Path controllerJar = extractEmbeddedPayload(project); Process process = new ProcessBuilder(command(controllerJar, forwarded)) @@ -137,20 +144,14 @@ static List command(Path controllerJar, String[] args) { } static String[] normalizedArguments(String[] args) { - Path currentProject = Path.of("").toAbsolutePath().normalize(); - if (args == null || args.length == 0) { - return new String[]{"ui", currentProject.toString()}; - } - if (args.length == 1 && PROJECT_COMMANDS.contains(args[0])) { - return new String[]{args[0], currentProject.toString()}; - } - return args.clone(); + return WorkbenchCommandLine.parse(args).forwarded(); } private static Path projectRoot(String[] args) { - if (args.length == 2 - && PROJECT_COMMANDS.contains(args[0]) - && !args[1].isBlank()) { + if (args.length >= 2 + && WorkbenchCommandLine.isForwardedCommand(args[0]) + && !args[1].isBlank() + && !args[1].startsWith("-")) { return Path.of(args[1]).toAbsolutePath().normalize(); } return Path.of("").toAbsolutePath().normalize(); diff --git a/src/main/java/tools/dscode/launcher/WorkbenchAgentCommands.java b/src/main/java/tools/dscode/launcher/WorkbenchAgentCommands.java new file mode 100644 index 00000000..174cc50d --- /dev/null +++ b/src/main/java/tools/dscode/launcher/WorkbenchAgentCommands.java @@ -0,0 +1,135 @@ +package tools.dscode.launcher; + +import tools.dscode.common.reporting.diagnostic.AgentDiscoverPlanner; +import tools.dscode.common.reporting.diagnostic.ConsumerMavenTestRunner; +import tools.dscode.common.reporting.diagnostic.DiagnosticCli; +import tools.dscode.common.reporting.diagnostic.LastDiscoverSnapshot; + +import java.io.PrintStream; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +/** Agent-facing Workbench verbs that run in the consumer JVM (not the controller JAR). */ +public final class WorkbenchAgentCommands { + private WorkbenchAgentCommands() { + } + + public static int run(String[] args, PrintStream out, PrintStream err) { + return run(args, out, err, ConsumerMavenTestRunner::run); + } + + static int run( + String[] args, + PrintStream out, + PrintStream err, + MavenRunner maven + ) { + WorkbenchCommandLine.Parsed parsed = WorkbenchCommandLine.parse(args); + try { + return switch (parsed.command()) { + case "export-guidance" -> DiagnosticCli.run( + new String[]{"export-guidance", parsed.outputDirectory().toString()}, + out, + err + ); + case "hint", "discover-hint" -> hint(parsed, out); + case "discover" -> discover(parsed, out, err, maven); + case "confirm" -> confirm(parsed, out, err, maven); + default -> { + err.println("Unknown Workbench agent command: " + parsed.command()); + yield 2; + } + }; + } catch (RuntimeException failure) { + err.println("Workbench " + parsed.command() + " failed: " + failure.getMessage()); + return 1; + } + } + + private static int hint(WorkbenchCommandLine.Parsed parsed, PrintStream out) { + AgentDiscoverPlanner.Plan plan = AgentDiscoverPlanner.discover( + parsed.project(), parsed.tags(), parsed.name() + ); + out.println("Recommended complete diagnostic Discover `pkb_runvars` (Workbench honors the project browser ladder; headed Chrome / pretty / @all project defaults do not sneak in):"); + out.println("pkb_runvars=" + plan.runVars()); + out.println(); + out.println("Browser: " + plan.browser().browser() + " (" + plan.browser().reason() + ")."); + out.println("Multi-scenario Discover/Confirm use this high pkb_parallel. Isolate stays one paused scenario — do not parallelize isolate."); + out.println("After Discover, isolate and confirm replay the retained pkb_run_profile through pkb_runvars. Never supply pkb_run_profile as input."); + out.println(); + out.println("NEXT: run discover"); + return 0; + } + + private static int discover( + WorkbenchCommandLine.Parsed parsed, + PrintStream out, + PrintStream err, + MavenRunner maven + ) { + AgentDiscoverPlanner.Plan plan = AgentDiscoverPlanner.discover( + parsed.project(), parsed.tags(), parsed.name() + ); + out.println("Workbench discover " + plan.browser().reason() + "."); + out.println("pkb_runvars=" + plan.runVars()); + List command = ConsumerMavenTestRunner.command(parsed.project(), plan.runVars()); + int exit = maven.run(parsed.project(), command, out, err); + return recordDiscover(parsed.project(), out, err, exit); + } + + private static int confirm( + WorkbenchCommandLine.Parsed parsed, + PrintStream out, + PrintStream err, + MavenRunner maven + ) { + LastDiscoverSnapshot.Snapshot snapshot = LastDiscoverSnapshot.require(parsed.project()); + Map retained = LastDiscoverSnapshot.retainedRunVars(snapshot); + String runVars = AgentDiscoverPlanner.confirmRunVars(retained, parsed.tags(), parsed.name()); + out.println("Workbench confirm replaying Discover snapshot as pkb_runvars."); + out.println("pkb_runvars=" + runVars); + List command = ConsumerMavenTestRunner.confirmCommand(parsed.project(), runVars); + int exit = maven.run(parsed.project(), command, out, err); + return printCatalog(parsed.project(), out, err, exit, "workbench-confirm", false); + } + + private static int recordDiscover(Path project, PrintStream out, PrintStream err, int mavenExit) { + return printCatalog(project, out, err, mavenExit, "workbench-discover", true); + } + + private static int printCatalog( + Path project, + PrintStream out, + PrintStream err, + int mavenExit, + String purpose, + boolean writeSnapshot + ) { + try { + LastDiscoverSnapshot.CatalogRun latest = LastDiscoverSnapshot.latestCatalogRun(project, purpose); + if (latest == null) latest = LastDiscoverSnapshot.latestCatalogRun(project); + if (latest == null || latest.runProfile() == null || latest.runProfile().isBlank()) { + err.println("Workbench finished but run-catalog.json has no retained pkb_run_profile."); + return mavenExit == 0 ? 1 : mavenExit; + } + if (writeSnapshot) { + LastDiscoverSnapshot.write(project, latest.runId(), latest.catalog(), latest.runProfile()); + } + out.println("run-catalog.json: " + latest.catalog()); + out.println("retained pkb_run_profile: " + latest.runProfile()); + if (writeSnapshot) { + out.println("NEXT: isolate a known failing scenario, then confirm."); + } + return mavenExit; + } catch (Exception failure) { + err.println("Could not read retained pkb_run_profile: " + failure.getMessage()); + return mavenExit == 0 ? 1 : mavenExit; + } + } + + @FunctionalInterface + interface MavenRunner { + int run(Path project, List command, PrintStream out, PrintStream err); + } +} diff --git a/src/main/java/tools/dscode/launcher/WorkbenchCommandLine.java b/src/main/java/tools/dscode/launcher/WorkbenchCommandLine.java new file mode 100644 index 00000000..1f00902c --- /dev/null +++ b/src/main/java/tools/dscode/launcher/WorkbenchCommandLine.java @@ -0,0 +1,126 @@ +package tools.dscode.launcher; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +/** Parses Maven-exec-friendly Workbench launcher arguments. */ +final class WorkbenchCommandLine { + static final Set FORWARDED_COMMANDS = Set.of( + "sync", "status", "worker-check", "live-check", "ui", "mcp", "isolate" + ); + static final Set AGENT_CORE_COMMANDS = Set.of( + "export-guidance", "hint", "discover-hint", "discover", "confirm" + ); + + private WorkbenchCommandLine() { + } + + record Parsed( + String command, + Path project, + Path outputDirectory, + String tags, + String name, + String[] forwarded + ) { + } + + static boolean isAgentCoreCommand(String command) { + return command != null && AGENT_CORE_COMMANDS.contains(command); + } + + static boolean isForwardedCommand(String command) { + return command != null && FORWARDED_COMMANDS.contains(command); + } + + static Parsed parse(String[] args) { + Path cwd = Path.of("").toAbsolutePath().normalize(); + if (args == null || args.length == 0) { + return new Parsed("ui", cwd, null, null, null, new String[]{"ui", cwd.toString()}); + } + String command = args[0]; + if ("export-guidance".equals(command)) { + Path output = args.length >= 2 && !isFlag(args[1]) + ? Path.of(args[1]) + : Path.of(".pickleball"); + return new Parsed(command, cwd, output, null, null, args.clone()); + } + + String tags = null; + String name = null; + Path project = null; + List rest = new ArrayList<>(); + for (int index = 1; index < args.length; index++) { + String token = args[index]; + if (token == null) continue; + if (token.startsWith("--tags=")) { + tags = token.substring("--tags=".length()); + continue; + } + if ("--tags".equals(token) && index + 1 < args.length) { + tags = args[++index]; + continue; + } + if (token.startsWith("--name=")) { + name = token.substring("--name=".length()); + continue; + } + if ("--name".equals(token) && index + 1 < args.length) { + name = args[++index]; + continue; + } + if (isFlag(token)) { + rest.add(token); + continue; + } + // Maven exec splits unquoted --name=The failing scenario into extra tokens. + if (name != null && !looksLikeProject(token)) { + name = name + " " + token; + continue; + } + if (project == null && looksLikeProject(token)) { + project = Path.of(token).toAbsolutePath().normalize(); + continue; + } + rest.add(token); + } + if (project == null) project = cwd; + + if (isForwardedCommand(command)) { + List forwarded = new ArrayList<>(); + forwarded.add(command); + forwarded.add(project.toString()); + if (tags != null && !tags.isBlank()) { + forwarded.add("--tags"); + forwarded.add(tags); + } + if (name != null && !name.isBlank()) { + forwarded.add("--name"); + forwarded.add(name); + } + forwarded.addAll(rest); + return new Parsed(command, project, null, tags, name, forwarded.toArray(String[]::new)); + } + return new Parsed(command, project, null, tags, name, args.clone()); + } + + private static boolean isFlag(String token) { + return token.startsWith("-") && token.length() > 1 && !looksLikePath(token); + } + + private static boolean looksLikePath(String token) { + String lower = token.toLowerCase(Locale.ROOT); + return lower.startsWith("-d") || token.contains("/") || token.contains("\\"); + } + + private static boolean looksLikeProject(String token) { + if (token == null || token.isBlank()) return false; + if (".".equals(token) || "..".equals(token)) return true; + if (token.contains("/") || token.contains("\\")) return true; + return Files.isDirectory(Path.of(token)); + } +} diff --git a/src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java b/src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java index 3ec82c0c..4cdf96af 100644 --- a/src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java +++ b/src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java @@ -60,7 +60,12 @@ public static int resolve(String configured) { } public static String recommendedDiscoverRunVars() { - return "pkb_browser=CHROME_HEADLESS, pkb_parallel=" + estimate() + return recommendedDiscoverRunVars("CHROME_HEADLESS"); + } + + public static String recommendedDiscoverRunVars(String browser) { + String selected = browser == null || browser.isBlank() ? "CHROME_HEADLESS" : browser.trim(); + return "pkb_browser=" + selected + ", pkb_parallel=" + estimate() + ", pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed"; } } diff --git a/src/main/java/tools/dscode/testengine/PKB_props.java b/src/main/java/tools/dscode/testengine/PKB_props.java index 4801656b..0f2de2da 100644 --- a/src/main/java/tools/dscode/testengine/PKB_props.java +++ b/src/main/java/tools/dscode/testengine/PKB_props.java @@ -156,6 +156,16 @@ public static void profileDefinition(String profileName, String assignments) { /** Canonical deterministic serialization of the resolved RunVars actually used for this run. */ public static String runProfile() { return get(PKB_RUN_PROFILE); } + /** Parse a compact RunVar assignment string such as a retained {@code pkb_run_profile}. */ + public static LinkedHashMap parseAssignments(String compact) { + return PickleballProfiles.parseAssignments(compact); + } + + /** Serialize execution RunVars in the same deterministic compact form as {@code pkb_run_profile}. */ + public static String serializeRunVars(Map values) { + return PickleballProfiles.serializeRunProfile(values); + } + /** Preferred direct RunVar input. Missing execution-context keys inherit; explicit blanks suppress inheritance. */ public static void runVars(String assignments) { clearDirectRunControls(); diff --git a/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md b/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md index 467090e8..25a8e3f5 100644 --- a/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md +++ b/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md @@ -2,45 +2,37 @@ This is the canonical AI-agent contract for projects that consume Pickleball as a Maven dependency. -A consumer project may contain only a short `AGENTS.md` bridge. That bridge can use Pickleball's `DiagnosticCli export-guidance` command to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. +A consumer project may contain only a short `AGENTS.md` bridge. That bridge uses Pickleball Workbench (`tools.dscode.launcher.PickleballWorkbenchLauncher`) `export-guidance` to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. ## Tool chooser -### Access probe +Pickleball Workbench is the one front door. It is a Java/Maven program, not an IDE feature and not a GUI requirement. Do not start the GUI. Do not register IDE MCP. -Prefer shell and Maven. This path is CLI/Maven-primary. Do not configure or self-register IDE MCP. Do not start an IDE-owned stdio server. +If `workbench_*` tools are already present in this session, use them as an alias for the same Isolate loop. Otherwise keep using Workbench CLI verbs. Missing `workbench_*` tools is not a reason to skip Discover. -If `workbench_*` tools are already present in this session, you may use them as an optional alias for the same live isolation loop. If they are absent, do not try to make the IDE own a stdio process. Continue with Discover using `mvn test` and the diagnostic JSON files. For Isolate, follow CLI/Workbench steps in the live loop when a Workbench session already exists; otherwise stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not silently use `mvn test` as isolate/debug. +From the consumer project, with Pickleball on the test classpath (`classpathScope=test`): -Use this order. Consumer AI agents for this Pickleball release discover failures with diagnostic `mvn test`. Live Workbench MCP is an optional alias when `workbench_*` tools are already connected. Do not start the Workbench GUI. +1. **Discover** — `PickleballWorkbenchLauncher discover` (optional `--tags` / `--name`). Workbench applies complete AI `pkb_runvars`: browser ladder, high/auto parallel, diagnostic, warn, failed retention. It wraps consumer `mvn test`. Do not start a live worker to run the whole suite. Then read `run-catalog.json` and the retained `pkb_run_profile`. +2. **Isolate** — `PickleballWorkbenchLauncher isolate` with `--tags` / `--name` for a known scenario. Replays the last Discover `pkb_run_profile` as `pkb_runvars` (never supply `pkb_run_profile` as input). One paused scenario; do not parallelize isolate. If isolate cannot obtain a live worker, tell the human that Workbench CLI isolate failed. +3. **Confirm** — `PickleballWorkbenchLauncher confirm` with the same Discover snapshot and narrow tags/name. +4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. -1. **Discover** — when you do not yet know which scenarios fail, or you need many/parallel scenarios: run one diagnostic `mvn test` with a **complete** `pkb_runvars` that always includes `pkb_browser=CHROME_HEADLESS`, a high `pkb_parallel` (the conservative JVM estimate printed by `discover-hint`, or `pkb_parallel=auto`), `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, plus the narrowest useful `pkb_tags`/`pkb_name`. Multi-scenario Discover and Confirm MUST use headless Chrome and high parallelism. Isolate / live Workbench stays one paused scenario; do not parallelize isolate. Use headed Chrome only when the scenario actually needs a window. Set those keys explicitly so project headed-chrome / pretty / `@all` defaults do not sneak in. Execution-context keys (`pkb_glue`, `pkb_features`, `pkb_datapath`, `pkb_callpath`, `pkb_componentpath`, `pkb_configpath`) may still inherit; after the run, read `pkb_run_profile` from `run-catalog.json` / `run-index.json` / `summary.json` as the complete resolved list, including inherited glue/features and the integer `pkb_parallel`. Do not assume omitted keys equal project pickleball.properties. This is not a skip of Workbench; it is how you find failures. Do not start a worker just to run the whole suite. After the run, open `run-catalog.json`, then only the relevant `run-index.json` / `summary.json`. If `workbench_*` tools already exist, you may instead read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary`. Do not ingest `events.jsonl` or screenshots to find which scenarios failed. Do not `workbench_worker_start` to read the catalog. -2. **Isolate / debug a known failing scenario** — after discovery has named the trouble spots, isolate those scenarios with live Workbench. If `workbench_*` tools are already present, use the live MCP loop below. If they are absent, do not invent IDE MCP and do not start an IDE-owned stdio server. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not keep using `mvn test` for isolation/debug. -3. **Confirm after isolation** — one bounded diagnostic `mvn test` with the same complete AI `pkb_runvars` (`pkb_browser=CHROME_HEADLESS`, high `pkb_parallel` when more than one scenario, `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, narrowest useful `pkb_tags`/`pkb_name`). -4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. Change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. - -Discover must work with zero MCP. Missing `workbench_*` tools is not a reason to skip Discover, and it is not a reason to self-register IDE MCP. A multi-scenario diagnostic test is a reason to run `mvn test` first; do not start a worker to run the whole suite. +`hint` (alias `discover-hint`) prints the recommended Discover `pkb_runvars` and `NEXT: run discover`. `mcp` and `ui` are host/human commands. Agents must not use `ui`. Hosts may already wire `mcp .`; that is optional host wiring, mentioned once, not an agent setup step. Do not copy consumer features into `.pickleball` as a sandbox. ### Live isolation loop -This loop isolates a **known** failing scenario after Discover. Hosts that already run Workbench MCP may wire `tools.dscode.launcher.PickleballWorkbenchLauncher` with `classpathScope=test` and args `mcp .` (not a GUI command). That is optional host wiring, not an agent setup step. Agents must not configure, self-register, or start an IDE-owned stdio MCP process. - -From the consumer project, with Pickleball on the test classpath: +This loop isolates a **known** failing scenario after Discover. Prefer Workbench CLI `isolate`. If `workbench_*` tools are already in this session, they are the same loop (`workbench_sync` / `workbench_worker_start` must use the Discover snapshot replayed as `pkb_runvars`). -1. If `workbench_*` tools are already in this session, continue with `workbench_sync` / `workbench_worker_start`. If they are absent, do not invent IDE MCP. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. -2. Call `workbench_sync` once. The agent must call it; Workbench does not auto-watch. Full compile when there is no live classpath or when Java/`pom`/dependencies changed; resources-only for feature/config/data; skip when unchanged. Live buffer edits need no sync. -3. `workbench_worker_start` — reuse the compiled live classpath; do not rebuild to start a worker. -4. `workbench_request_control` -5. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. -6. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`; screenshot bytes are expensive in agent context. -7. When you need a retained evidence pack, run **one** diagnostic `mvn test` with `pkb_runvars` (below). Read that pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` instead of globbing `reports/diagnostic-runs`. -8. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. Do not paste the report body, cause/fix essays, or screenshots into the chat panel. +1. Workbench `isolate` (CLI) or, when already connected, `workbench_sync` then `workbench_worker_start`. +2. `workbench_request_control` when using the MCP alias. +3. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. +4. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`. +5. Confirm with Workbench `confirm`. Read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` when those tools already exist. +6. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. -`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Insert, nest, or retry in the same paused browser/Mapping state. MCP `isError=true` is for controller/runtime problems such as a missing paused worker; do not restart the worker merely because a step failed. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). - -Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`); they do not require Maven. +`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`). ### Generated trees are not the project @@ -54,7 +46,7 @@ Worker restart without rebuild already exists (`workbench_worker_restart`). Step Keep first-read small. After a successful export: 1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Stay in this guide's tool chooser: Discover with a diagnostic `mvn test` when the failing scenario is unknown (zero MCP is enough). Isolate a known failure with already-connected `workbench_*` tools or a Workbench CLI session; do not self-register IDE MCP. +2. Stay in this guide's tool chooser: Workbench `discover` when the failing scenario is unknown. Isolate a known failure with Workbench `isolate`, or with already-connected `workbench_*` tools as the same loop. Do not start the GUI. 3. Inspect the **real** consumer `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. 4. Open a specific exported guide only when that topic is needed, for example `docs/dynamic-steps.md`, `docs/diagnostic-reporting.md`, `docs/configuration.md`, or `docs/ai-run-configuration.md`. 5. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. @@ -146,25 +138,25 @@ Never supply `pkb_run_profile` or `pkb_run_profile.` as input. They are When you launch Pickleball tests and the intended execution settings are known, use `pkb_runvars` as the authoritative input. Put intentional tag/name selection, browser, evidence/logging controls, and other non-secret RunVar changes inside `pkb_runvars`; do not default to ambient optional project settings or separate JVM `-Dpkb_*` RunVars. Use `pkb_profile` or ordinary JVM RunVar overrides only when the task specifically tests those configuration semantics or the user asks for them. Keep protected secrets and diagnostic lineage outside `pkb_runvars`. -For an agent's bounded confirmation `mvn test` (not the human runner defaults), include diagnostic evidence controls, headless Chrome, and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars`: +For an agent's bounded confirmation (not the human runner defaults), include diagnostic evidence controls, the browser ladder (keep a remote `pkb_browser`; otherwise prefer `CHROME_HEADLESS`), and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars` keys: ```text -pkb_browser=CHROME_HEADLESS +pkb_browser= pkb_parallel= pkb_reportingmode=diagnostic pkb_loglevel=warn pkb_reportretention=failed ``` -Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. `DiagnosticCli discover-hint` prints the estimated integer `pkb_parallel` for the current JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. +Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. Workbench `hint` prints the estimated integer `pkb_parallel` and the selected browser for the current project/JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. -These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after a live-loop isolation: +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after isolation: ```text -mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_parallel=auto, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +PickleballWorkbenchLauncher confirm --tags=@the-failing-tag --name='The failing scenario' ``` -After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. +After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. Isolate and confirm replay that retained profile through `pkb_runvars`; they do not silently re-resolve from project defaults. A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: @@ -222,21 +214,31 @@ After isolation and the diagnostic rerun, emit a small human handoff. JSON is th ## Diagnostic utility commands -From a Maven consumer where Pickleball is on the test classpath: +The agent-facing name is Workbench. From a Maven consumer where Pickleball is on the test classpath: + +```text +PickleballWorkbenchLauncher export-guidance .pickleball +PickleballWorkbenchLauncher hint +PickleballWorkbenchLauncher discover [--tags ] [--name ] +PickleballWorkbenchLauncher isolate [--tags ] [--name ] +PickleballWorkbenchLauncher confirm [--tags ] [--name ] +``` + +`DiagnosticCli` remains the implementation behind export-guidance/hint and the comparison/rebuild utilities: ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print that DiagnosticCli list and state that Workbench is the agent entry. -Use `guidance` to print this guide, `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, and `discover-hint` for the complete diagnostic `mvn test` `pkb_runvars` one-liner (headless Chrome, estimated `pkb_parallel`, diagnostic evidence controls) plus `run-catalog.json` / `pkb_run_profile` next step. Prefer `DiagnosticCli` over constructing Maven classpaths and JShell scripts for routine diagnostic operations. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. +Use Workbench `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, `hint` for the complete Discover `pkb_runvars` (browser ladder, estimated `pkb_parallel`, diagnostic evidence controls), and `discover` / `isolate` / `confirm` for the turnkey loop. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. ## Controlled diagnostic reruns @@ -300,14 +302,16 @@ If the consumer is nested inside the Pickleball source repository, repository-le For a normal external consumer, do not assume those core files exist. -## Maintainer pointer-eval harness +## Maintainer-only: pointer-eval harness + +This section is for Pickleball maintainers. It is not the product suite and is not first-read for consumer agents. -Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. +Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses Workbench discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. During an eval, still follow Discover / Isolate / Confirm; do not ignore canned fails. Run it explicitly: ```text -mvn test -Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS" +PickleballWorkbenchLauncher discover --tags=@agent-pointer-eval ``` -or `-Dpkb_tags=@agent-pointer-eval`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. +or `-Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS"`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/README.md b/src/main/resources/META-INF/pickleball/guidance/docs/README.md index 3d8d3cb7..83114900 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/README.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/README.md @@ -2,7 +2,7 @@ Pickleball extends Cucumber with a dynamic feature-file language while preserving normal Cucumber behavior. The pages below describe the supported authoring model and link to real executable examples in [`maven-consumer-project`](consumer-project.md). -When these docs are materialized from the Maven dependency with `DiagnosticCli export-guidance`, links to `../maven-consumer-project/...` resolve to the version-matched, read-only reference snapshot exported beside the docs. Human readers can inspect those working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. Consumer AI agents should follow `.pickleball/AGENT-GUIDE.md` first and open a specific guide only when needed. +When these docs are materialized from the Maven dependency with Workbench `export-guidance`, links to `../maven-consumer-project/...` resolve to the version-matched, read-only reference snapshot exported beside the docs. Human readers can inspect those working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. Consumer AI agents should follow `.pickleball/AGENT-GUIDE.md` first and open a specific guide only when needed. ## Start here diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/agent/README.md b/src/main/resources/META-INF/pickleball/guidance/docs/agent/README.md index 14968b77..5ef38667 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/agent/README.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/agent/README.md @@ -18,9 +18,9 @@ This directory supports repository-native AI coding agents. It is not a runtime Agent adapters should remain small and point back to the canonical contract rather than copying the full project description. -The nested `/maven-consumer-project/AGENTS.md` is a dependency-owned guidance bootstrap plus a short discover-vs-isolate pointer. It materializes version-matched guidance, directs the consumer agent to `.pickleball/AGENT-GUIDE.md`, and states that agents discover with a diagnostic `mvn test`; MCP is optional if already connected, and agents do not self-register IDE MCP. Refresh/version/manifest semantics, authoring rules, configuration, diagnostics, and troubleshooting belong in the exported dependency guidance. +The nested `/maven-consumer-project/AGENTS.md` is a dependency-owned Workbench bootstrap plus a short Discover/Isolate/Confirm pointer. It materializes version-matched guidance through `PickleballWorkbenchLauncher export-guidance`, directs the consumer agent to `.pickleball/AGENT-GUIDE.md`, and tells agents to use Workbench `hint` / `discover` / `isolate` / `confirm`. Do not start the GUI. Do not register IDE MCP. Refresh/version/manifest semantics, authoring rules, configuration, diagnostics, and troubleshooting belong in the exported dependency guidance. -The nested `/maven-consumer-project/.github/copilot-instructions.md` is the same bootstrap-plus-pointer for IntelliJ Copilot Chat, which reads that file rather than `AGENTS.md`. +The nested `/maven-consumer-project/.github/copilot-instructions.md` is identical to `AGENTS.md` so Copilot Chat sees the same Workbench pointer. The nested `/maven-consumer-project/README.md` is ordinary sample-project documentation. It may point humans and agents at `AGENTS.md` for guidance export, but should not duplicate the AI guidance lifecycle. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md b/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md index 8c1f7678..fa669e76 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/agent/feature-map.md @@ -8,7 +8,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab | Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records; `InvestigationHandoff` | `verifyProtocolIsolation`; `ControlBridgeClientTest`; `InvestigationHandoffTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.md`; `docs/diagnostic-reporting.md`; `pickleball-workbench/AGENTS.md` | | Pickleball Workbench synchronization / persistent live worker / MCP stdio / player-style Swing+WebView UI / watched-agent control lease / scenario name-tag filter / Text-Blocks editor toggle | `pickleball-workbench`; `WorkbenchApplication`; `WorkbenchRuntimeBoundary`; `WorkbenchServices`; `WorkbenchController`; `tools.dscode.workbench.lease`; `WorkbenchAttachServer`; `tools.dscode.workbench.sync`; `WorkbenchSyncPlanner`; `WorkbenchSyncInputs`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `workbench_diagnostic_catalog`; `workbench_diagnostic_run`; `workbench_diagnostic_summary`; `workbench_investigation_emit`; `tools.dscode.workbench.player`; `LiveEditorView`; `tools.dscode.workbench.catalog`; `ScenarioFilter`; `ConsumerFeatureCatalog`; `tools.dscode.workbench.mapping`; `tools.dscode.workbench.terminal`; `tools.dscode.workbench.diagnostics`; `tools.dscode.workbench.ui`; `FeaturePickerPanel`; OpenJFX `WebView` / `JFXPanel`; protocol `ControlProtocol.WORKER_MAIN_CLASS` | `:pickleball-workbench:test`; `WorkbenchRuntimeBoundaryTest`; `ConsumerFeatureCatalogTest`; `ScenarioFilterTest`; `GherkinBlockDocumentTest`; `LivePlaybackCoordinatorTest`; `LiveFeatureSaveTest`; `LiveEditorViewTest`; `WorkbenchControlLeaseTest`; `WorkbenchControllerLeaseTest`; `WorkbenchAttachServerTest`; `MappingValueCodecTest`; `WorkerLogBufferTest`; `DiagnosticEvidenceNavigatorTest`; `InvestigationHandoffTest`; `WorkbenchSynchronizerTest`; `WorkbenchSyncPlannerTest`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.md`; `docs/consumer-agent-guide.md`; `pickleball-workbench/AGENTS.md` | | Consumer-side Control Bridge and live investigation | worker-side `pickleball-control-api/src/main/java/tools/dscode/control/bridge`; core `ControlRuntime`; protocol `tools.dscode.control.protocol`; controller client `tools.dscode.workbench.bridge.ControlBridgeClient`; endpoints `/v1/status`, `/v1/scenarios`, `/v1/events`, `/v1/pause`, `/v1/resume`, `/v1/steps/execute`, `/v1/mappings/*`, `/v1/browser/*`, `/v1/services/call`, `/v1/breakpoints*`, `/v1/step-overrides*` | controller-only fake-server `ControlBridgeClientTest`; `control-bridge.feature` tagged `@control-bridge`; `ControlBridgeTestSteps.java`; Workbench `worker-check` / `live-check` | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` | -| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction | `PickleballWorkbenchLauncherTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` | +| Consumer Workbench launcher/extractor | `tools.dscode.launcher.PickleballWorkbenchLauncher`; `WorkbenchAgentCommands`; `ControlProtocol.EMBEDDED_WORKBENCH_RESOURCE`; root `shadowJar`; `verifyEmbeddedWorkbench`; content-addressed `.pickleball/workbench/controller//` extraction; agent verbs `export-guidance`, `hint`, `discover`, `isolate`, `confirm` | `PickleballWorkbenchLauncherTest`; `WorkbenchAgentCommandsTest`; `verifyEmbeddedWorkbench`; `verifyWorkbenchEntrypoint` | `docs/getting-started.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md`; `docs/consumer-agent-guide.md` | | Step Override runtime and Workbench authoring | `src/main/java/tools/dscode/control/override`; `src/main/java/io/cucumber/core/runner/StepOverrideDispatcher.java`; `NPickleStepTestStepFactory`; `src/main/aspectj/tools/dscode/control/override/StepOverrideLifecycleAspect.aj`; bridge `/v1/step-overrides*`; `ControlBridgeClient`; `WorkbenchLiveSession`; MCP `workbench_step_override_*` | `StepOverrideCompilerTest`; `StepOverrideChecks`; `StepOverrideBridgeTestSteps`; `@step-override`; Workbench `live-check`; `WorkbenchMcpServerTest`; `WorkbenchUiControllerTest` | `docs/step-overrides.md`; `docs/pickleball-workbench.md`; `pickleball-workbench/AGENTS.md` | | Dynamic control API and semantic hooks | `pickleball-control-api/src/main/java/tools/dscode/control/api`; `src/main/java/tools/dscode/common/control`; `src/main/aspectj/tools/dscode/common/control/ControlRuntimeAspect.aj`; search `DynamicControl`, `MappingControl`, `ElementControl`, `ServiceCallControl`, `ControlHook` | `DynamicControlApiChecks.java`; `ControlRuntimeObserverChecks.java`; `internal-framework-java-checks.feature`; `control-bridge.feature` | `docs/dynamic-control-api.md` | | Pickleball-native element inspection | `ElementControl.java`; `ElementInspection.java`; `ElementEvidence.java`; `ExecutionDictionary.java`; `DefinitionContext.java`; `BrowserSteps.getCurrentDriverIfPresent`; bridge `/v1/browser/elements`; Workbench `workbench_element_inspect` | `@control-bridge`; custom element/category consumer scenarios | `docs/dynamic-control-api.md`; `docs/pickleball-workbench.md`; `docs/custom-element-definitions.md` | @@ -18,8 +18,8 @@ This file maps consumer-visible capabilities to implementation anchors, executab | Selenium navigation and interaction | `BrowserSteps.java`; `NavigationSteps.java`; `ElementWrapper.java`; `HumanInteractions.java`; `SeleniumUtils.java`; `DriverConstruction.java`; bundled `META-INF/pickleball/configs/CHROME_HEADLESS.yaml`; `ParsingMap.initializeConfigs` fallback | `navigation.feature`; `forms-dynamic-steps.feature`; `dialogs.feature`; `ChromeHeadlessConfigChecks.java`; browser test-site pages | `docs/dynamic-steps.md`; `docs/custom-element-definitions.md`; `docs/configuration.md`; `docs/config-files-and-resource-mapping.md` | | Custom element definitions/catalog context | `ExecutionDictionary.java`; `ElementMatch.java`; consumer `PickleballTests.java`; search `category(`, `inheritsFrom` | `catalog-context.feature`; `forms-dynamic-steps.feature`; `site/catalog.html` | `docs/custom-element-definitions.md`; `docs/config-files-and-resource-mapping.md` | | Mapping, ParsingMap/NodeMap, templates/directives | `MappingSteps.java`; `FileAndDataParsing.java`; `MappingProcessor.java`; `QuoteParser.java`; `NodeMap.java`; `ParsingMap.java`; `ValueFormatting.java`; `common/dataelements` | `mapping-and-resources.feature`; `mapping-value-type-preservation.feature`; `scenario-data-references.feature`; Data Element features; `QuoteParserChecks.java`; internal Java checks | `docs/mapping-and-templating.md`; `docs/data-values-and-elements.md`; `docs/data-element-query-runtime.md`; `docs/config-files-and-resource-mapping.md` | -| Configuration/profiles/RunVars | `PKB_props.java`; `PickleballProfiles.java`; `PkbPropertyValueNormalizer.java`; `ParallelCountEstimator.java`; runner/config classes; search `pkb_profile`, `pkb_runvars`, `pkb_run_profile`, `pkb_configpath`, `pkb_parallel=auto` | `configuration-system-properties.feature`; `ProfileConfigurationChecks.java`; `ParallelCountEstimatorChecks.java`; consumer properties/profile examples | `docs/configuration.md`; `docs/getting-started.md`; `docs/ai-run-configuration.md`; `docs/consumer-project.md` | -| Consumer guidance export/reference snapshot | `DiagnosticCli.java`; `gradle/consumer-guidance.gradle`; `scripts/sync_consumer_guidance.py`; `maven-consumer-project/AGENTS.md`; `maven-consumer-project/.github/copilot-instructions.md`; search `export-guidance`, `discover-hint`, `GUIDANCE-MANIFEST.json`, `.pickleball/investigations` | `PickleballGuidanceChecks.java`; consumer guidance contract checks | `docs/consumer-agent-guide.md`; `docs/consumer-project.md` | +| Configuration/profiles/RunVars | `PKB_props.java`; `PickleballProfiles.java`; `PkbPropertyValueNormalizer.java`; `ParallelCountEstimator.java`; `AgentBrowserLadder.java`; `AgentDiscoverPlanner.java`; runner/config classes; search `pkb_profile`, `pkb_runvars`, `pkb_run_profile`, `pkb_configpath`, `pkb_parallel=auto` | `configuration-system-properties.feature`; `ProfileConfigurationChecks.java`; `ParallelCountEstimatorChecks.java`; `AgentBrowserLadderChecks.java`; consumer properties/profile examples | `docs/configuration.md`; `docs/getting-started.md`; `docs/ai-run-configuration.md`; `docs/consumer-project.md` | +| Consumer guidance export/reference snapshot | `DiagnosticCli.java`; `PickleballWorkbenchLauncher` `export-guidance`; `gradle/consumer-guidance.gradle`; `scripts/sync_consumer_guidance.py`; `maven-consumer-project/AGENTS.md`; `maven-consumer-project/.github/copilot-instructions.md`; search `export-guidance`, `hint`, `discover-hint`, `GUIDANCE-MANIFEST.json`, `.pickleball/investigations` | `PickleballGuidanceChecks.java`; consumer guidance contract checks | `docs/consumer-agent-guide.md`; `docs/consumer-project.md` | | Consumer AI-agent pointer-eval harness | `maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`; tag `@agent-pointer-eval` (optional `@agent-pointer-eval-pass` / `@agent-pointer-eval-fail`) | opt-in mixed pass/fail; must not carry `@all`, `@regression`, or other Maven suite-profile tags; `scripts/verify_agent_contract.py` | `docs/consumer-agent-guide.md` | | Diagnostic reporting and controlled reruns | `src/main/java/tools/dscode/common/reporting/diagnostic`; `InvestigationHandoff`; diagnostic aspects; `DiagnosticCli.java`; `emit-investigation`; `VisualFingerprintComparator.java`; `DiagnosticRunComparator.java`; catalog/summary `runProfile` | `DiagnosticReportingChecks.java`; `Diagnostic213CompletionChecks.java`; `InvestigationHandoffChecks.java`; diagnostic features | `docs/diagnostic-reporting.md`; `docs/ai-diagnostic-reporting-plan.md`; `docs/ai-run-configuration.md`; root `AGENTS.md` | | Nested steps/block conditionals | search `Nested`, `Conditional`, `Block`, `Condition` in core implementation | `nested-and-block-conditionals.feature` | `docs/nested-steps.md`; `docs/block-conditionals.md` | @@ -48,6 +48,6 @@ The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`, `WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. A `FAILED` `executeStep` result leaves that same paused worker available; it is not a worker-ending error. Step Override source is compiled and loaded worker-side. Normal live calls do not invoke Maven/Gradle, resynchronize, or restart the worker. -The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). Consumer AI agents discover unknown or multi-scenario failures with a diagnostic `mvn test` and `run-catalog.json` (zero MCP is enough). Live `workbench_*` MCP tools are an optional alias when already connected; agents must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with those tools or a Workbench CLI session; missing tools is a dead-end to tell the human, not a skip of Discover. UI mode may write a localhost attach endpoint to `.pickleball/workbench/attach.json` so a watcher can join a human GUI session instead of starting a second Workbench; that attach file is not the consumer-agent path for this release. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system. +The lightweight non-Spring stdio MCP adapter and player-style Swing/WebView UI both delegate through `WorkbenchServices` / `WorkbenchController`. `LiveScenarioPlayer` is a Workbench-side headless presentation model for the editable session buffer, stable line identities, selection, click-to-seek playhead, and player state only; it does not parse or execute Pickleball steps, model ParsingMap/NodeMap semantics, or claim runtime rewind. `LiveEditorView` is the Text vs Blocks presentation choice for that same buffer; toggling it must not change document text, selection, or playhead id. While `RUNNING`, `WorkbenchController.executeStep` owns playhead follow once (`LivePlaybackCoordinator.followExecutedStep`); the Swing Play loop continues without remaking that mark, and leftover marks of an already-consumed step are no-ops. The controller also owns the watched-agent control lease (`HUMAN` / `AGENT`, banner `currentAction`, gated Save permission). Consumer AI agents use Workbench `discover` / `isolate` / `confirm` as the one front door. Live `workbench_*` MCP tools are an optional alias when already connected; agents must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with Workbench CLI `isolate` or already-connected `workbench_*` tools. If isolate cannot obtain a live worker, tell the human that Workbench CLI isolate failed. UI mode may write a localhost attach endpoint to `.pickleball/workbench/attach.json` so a watcher can join a human GUI session instead of starting a second Workbench; that attach file is not the consumer-agent path for this release. The left-rail picker filters project-owned scenarios by name (starts with / contains / ends with / full match; default contains; case-insensitive) and Cucumber tags (include AND, exclude NOT, Feature/Rule/outline/Examples inheritance parsed from catalog `.feature` files without calling Cucumber). Feature-file selection is an optional collapsed secondary filter; with none selected, name/tag apply to every catalog scenario. The live editor is Gherkin text or a block WebView over that same player model. Global Play starts from the first executable step; Step Editor **Step** is isolated `executeStep`; **From Here** runs from the selected step; wait-at-end stays in play so Enter can append-and-continue. Mapping property types go through `mappingPut` / `mappingRestore`. Terminal tails existing worker log files. Diagnostic explorer reads retained `reports/diagnostic-runs` artifacts in the repository evidence order. The default buffer is a Workbench-owned browser demo against `URL.home`; **Save** is confirmation-gated and copies only a picker-loaded scenario back to its originating `.feature` file. OpenJFX is Workbench-only. MCP mode reserves stdout for newline-delimited MCP JSON-RPC and redirects ordinary output to stderr. The UI remains execution-oriented and does not recreate a project IDE, generic build/process UI, source navigator, or collaboration system. For this boundary, scenario validation is limited to the affected `@control-bridge` and/or `@step-override-bridge` tags with `pkb_parallel=80` where practical. Do not run `@all` for Workbench isolation changes. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md b/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md index 7ff607f3..5b4c9f58 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/agent/repository-index.md @@ -237,13 +237,17 @@ This inventory helps coding agents discover relevant files. It does not replace - `src/main/java/tools/dscode/common/mappings/ScenarioMapping.java` - `src/main/java/tools/dscode/common/mappings/StepMapping.java` - `src/main/java/tools/dscode/common/mappings/ValueFormatting.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadder.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlanner.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ConfigurationProvenance.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/ConsumerMavenTestRunner.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticCli.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticIndexRebuilder.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticReporter.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticRunComparator.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/DiagnosticRuntime.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ExplicitReportRegistry.java` +- `src/main/java/tools/dscode/common/reporting/diagnostic/LastDiscoverSnapshot.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ReportRetentionPolicy.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/ScenarioIdentity.java` - `src/main/java/tools/dscode/common/reporting/diagnostic/SourceProvenance.java` @@ -365,6 +369,8 @@ This inventory helps coding agents discover relevant files. It does not replace - `src/main/java/tools/dscode/coredefinitions/UtilitySteps.java` - `src/main/java/tools/dscode/cucumberextended/utilities/StringUtilities.java` - `src/main/java/tools/dscode/launcher/PickleballWorkbenchLauncher.java` +- `src/main/java/tools/dscode/launcher/WorkbenchAgentCommands.java` +- `src/main/java/tools/dscode/launcher/WorkbenchCommandLine.java` - `src/main/java/tools/dscode/misc/DummySteps.java` - `src/main/java/tools/dscode/parallelutilities/ParallelCountEstimator.java` - `src/main/java/tools/dscode/parallelutilities/Stagger.java` @@ -389,8 +395,11 @@ This inventory helps coding agents discover relevant files. It does not replace ## Framework tests +- `src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderTest.java` +- `src/test/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlannerTest.java` - `src/test/java/tools/dscode/control/override/StepOverrideCompilerTest.java` - `src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java` +- `src/test/java/tools/dscode/launcher/WorkbenchAgentCommandsTest.java` - `src/test/java/tools/dscode/parallelutilities/ParallelCountEstimatorTest.java` - `src/test/java/tools/dscode/testengine/DynamicSuiteBootstrapWorkbenchRootTest.java` @@ -484,6 +493,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalog.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/catalog/ScenarioFilter.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/discover/LastDiscoverSnapshot.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchCallContext.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLease.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/lease/WorkbenchControlLeaseSnapshot.java` @@ -547,6 +557,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ScenarioFilterTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/InvestigationHandoffTest.java` +- `pickleball-workbench/src/test/java/tools/dscode/workbench/discover/LastDiscoverSnapshotTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/lease/WorkbenchControlLeaseTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/mapping/MappingValueCodecTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchAttachServerTest.java` @@ -595,6 +606,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `maven-consumer-project/src/test/java/tools/dscode/common/driver/ChromeHeadlessConfigChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/mappings/MappingDataRefactorChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/mappings/QuoteParserChecks.java` +- `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/Diagnostic213CompletionChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/DiagnosticReportingChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/common/reporting/diagnostic/InvestigationHandoffChecks.java` @@ -603,6 +615,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `maven-consumer-project/src/test/java/tools/dscode/common/util/datetime/BusinessTimePostModifierChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/coredefinitions/DataTableConversionChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/coredefinitions/ModularScenariosChecks.java` +- `maven-consumer-project/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/parallelutilities/ParallelCountEstimatorChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/testengine/PkbPropertyValueNormalizerChecks.java` - `maven-consumer-project/src/test/java/tools/dscode/testengine/ProfileConfigurationChecks.java` diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/ai-diagnostic-reporting-plan.md b/src/main/resources/META-INF/pickleball/guidance/docs/ai-diagnostic-reporting-plan.md index f8891e79..c80ec593 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/ai-diagnostic-reporting-plan.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/ai-diagnostic-reporting-plan.md @@ -132,13 +132,13 @@ For a source-only fix, reuse the retained RunVars unchanged and omit `pkb_change ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print this list. The agent-facing entry is Pickleball Workbench. See `docs/diagnostic-reporting.md` for evidence use, `docs/ai-run-configuration.md` for controlled execution, and `docs/diagnostic-lineage-metadata.md` for investigation metadata. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/ai-run-configuration.md b/src/main/resources/META-INF/pickleball/guidance/docs/ai-run-configuration.md index 6201a14e..f38c6d3f 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/ai-run-configuration.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/ai-run-configuration.md @@ -323,18 +323,18 @@ When operating in a consumer project: ## AI agents -Set a **complete** Discover `pkb_runvars` rather than a partial overlay: +The agent-facing entry is Pickleball Workbench (`hint`, `discover`, `isolate`, `confirm`). Set a **complete** Discover `pkb_runvars` rather than a partial overlay. Workbench `hint` prints the browser-ladder result and estimated integer parallel count. The browser ladder keeps a remote project `pkb_browser` (`SAUCE_*` / `GRID_*` / `REMOTE_*`); otherwise it prefers `CHROME_HEADLESS`. Unused Sauce/Grid yaml files are not auto-selected. ```text -pkb_browser=CHROME_HEADLESS +pkb_browser= pkb_parallel= pkb_reportingmode=diagnostic pkb_loglevel=warn pkb_reportretention=failed ``` -plus the narrowest useful `pkb_tags` / `pkb_name`. `DiagnosticCli discover-hint` prints the estimated integer parallel count for the current JVM. Multi-scenario Discover/Confirm must use headless Chrome and high parallelism. Isolate / live Workbench stays one paused scenario. +plus the narrowest useful `pkb_tags` / `pkb_name`. Multi-scenario Discover/Confirm use that high parallelism. Isolate / live Workbench stays one paused scenario. -After the run, inspect `pkb_run_profile` from `run-catalog.json`, `run-index.json`, or `summary.json`. That output is the complete resolved RunVar list after inheritance (glue/features/data/call/component/configpath) and after `pkb_parallel=auto` is stamped as an integer. Do not assume omitted `pkb_runvars` keys equal project `pickleball.properties` — optional keys such as headed Chrome, `pretty`, and `pkb_tags=@all` do not leak into a controlled run, which is why agents must set the Discover keys explicitly. +After Discover, inspect `pkb_run_profile` from `run-catalog.json`, `run-index.json`, or `summary.json`. Isolate and confirm replay that retained profile through `pkb_runvars`. If there is no prior Discover snapshot, Workbench says so; it does not silently re-resolve from project defaults. Never supply `pkb_run_profile` as input. Workbench MCP `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` return the same retained `runProfile` when present. The consumer worker resolves the same snapshot internally through `PickleballRunner`; it does not accept `pkb_run_profile` as input. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/configuration.md b/src/main/resources/META-INF/pickleball/guidance/docs/configuration.md index 456c4a5c..6e28fb04 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/configuration.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/configuration.md @@ -356,7 +356,7 @@ max(2, min(availableProcessors, floor(maxMemoryMB / 512), 24)) Chrome workers are RAM-heavy, so a 32-core / 64GiB box does not blindly pick 32 workers; the hard cap is 24, and heap can cap lower. Tiny heaps resolve to 2. An explicit numeric `pkb_parallel` is never overwritten. Omitting `pkb_parallel` does not enable parallel execution. -The resolved integer is stamped into the final RunVars and `pkb_run_profile`. `DiagnosticCli discover-hint` prints that estimated number in the recommended Discover `pkb_runvars` command. +The resolved integer is stamped into the final RunVars and `pkb_run_profile`. Workbench `hint` prints that estimated number in the recommended Discover `pkb_runvars` command. ## Bundled `CHROME_HEADLESS` @@ -367,6 +367,16 @@ The resolved integer is stamped into the final RunVars and `pkb_run_profile`. `D The bundled headless config uses `--headless=new`, a fixed `--window-size=1920,1080`, no `MAXIMIZE`, and `QUIT_LOCAL_DRIVER`. Consumer `CHROME`, `EDGE`, `GRID`, and `SAUCE` yaml files are unchanged. Agents can set `pkb_browser=CHROME_HEADLESS` without copying yaml into the project. +## Agent Discover browser ladder + +Workbench Discover/Confirm do not blindly MUST-use `CHROME_HEADLESS` for every project: + +1. If `default_profile` / runner / retained `pkb_run_profile` `pkb_browser` is already a remote farm name (`SAUCE_*`, `GRID_*`, `REMOTE_*`, or clearly non-local), keep it. Those consumers run exclusively on the external farm. +2. Otherwise prefer `CHROME_HEADLESS` (consumer yaml if present, else the JAR-bundled config above). +3. If local headless cannot start and the project already defines and uses GRID/SAUCE/REMOTE as its `pkb_browser`, fall back to that project browser. Do not pick Sauce/Grid merely because unused yaml files exist in `configs/`. + +Isolate stays one scenario and does not raise `pkb_parallel`. + ## Cucumber aliases Pickleball synchronizes its main selection aliases with Cucumber properties, including: diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/consumer-agent-guide.md b/src/main/resources/META-INF/pickleball/guidance/docs/consumer-agent-guide.md index 467090e8..25a8e3f5 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/consumer-agent-guide.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/consumer-agent-guide.md @@ -2,45 +2,37 @@ This is the canonical AI-agent contract for projects that consume Pickleball as a Maven dependency. -A consumer project may contain only a short `AGENTS.md` bridge. That bridge can use Pickleball's `DiagnosticCli export-guidance` command to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. +A consumer project may contain only a short `AGENTS.md` bridge. That bridge uses Pickleball Workbench (`tools.dscode.launcher.PickleballWorkbenchLauncher`) `export-guidance` to materialize the version-matched guidance embedded in the installed Pickleball dependency. When this file is materialized as `.pickleball/AGENT-GUIDE.md`, supporting documentation is under `.pickleball/docs/` and a curated reference snapshot of Pickleball's executable Maven consumer is under `.pickleball/maven-consumer-project/`. Full `docs/` and the snapshot stay exported for on-demand lookup. Do not dump them into first-read context. ## Tool chooser -### Access probe +Pickleball Workbench is the one front door. It is a Java/Maven program, not an IDE feature and not a GUI requirement. Do not start the GUI. Do not register IDE MCP. -Prefer shell and Maven. This path is CLI/Maven-primary. Do not configure or self-register IDE MCP. Do not start an IDE-owned stdio server. +If `workbench_*` tools are already present in this session, use them as an alias for the same Isolate loop. Otherwise keep using Workbench CLI verbs. Missing `workbench_*` tools is not a reason to skip Discover. -If `workbench_*` tools are already present in this session, you may use them as an optional alias for the same live isolation loop. If they are absent, do not try to make the IDE own a stdio process. Continue with Discover using `mvn test` and the diagnostic JSON files. For Isolate, follow CLI/Workbench steps in the live loop when a Workbench session already exists; otherwise stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not silently use `mvn test` as isolate/debug. +From the consumer project, with Pickleball on the test classpath (`classpathScope=test`): -Use this order. Consumer AI agents for this Pickleball release discover failures with diagnostic `mvn test`. Live Workbench MCP is an optional alias when `workbench_*` tools are already connected. Do not start the Workbench GUI. +1. **Discover** — `PickleballWorkbenchLauncher discover` (optional `--tags` / `--name`). Workbench applies complete AI `pkb_runvars`: browser ladder, high/auto parallel, diagnostic, warn, failed retention. It wraps consumer `mvn test`. Do not start a live worker to run the whole suite. Then read `run-catalog.json` and the retained `pkb_run_profile`. +2. **Isolate** — `PickleballWorkbenchLauncher isolate` with `--tags` / `--name` for a known scenario. Replays the last Discover `pkb_run_profile` as `pkb_runvars` (never supply `pkb_run_profile` as input). One paused scenario; do not parallelize isolate. If isolate cannot obtain a live worker, tell the human that Workbench CLI isolate failed. +3. **Confirm** — `PickleballWorkbenchLauncher confirm` with the same Discover snapshot and narrow tags/name. +4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. -1. **Discover** — when you do not yet know which scenarios fail, or you need many/parallel scenarios: run one diagnostic `mvn test` with a **complete** `pkb_runvars` that always includes `pkb_browser=CHROME_HEADLESS`, a high `pkb_parallel` (the conservative JVM estimate printed by `discover-hint`, or `pkb_parallel=auto`), `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, plus the narrowest useful `pkb_tags`/`pkb_name`. Multi-scenario Discover and Confirm MUST use headless Chrome and high parallelism. Isolate / live Workbench stays one paused scenario; do not parallelize isolate. Use headed Chrome only when the scenario actually needs a window. Set those keys explicitly so project headed-chrome / pretty / `@all` defaults do not sneak in. Execution-context keys (`pkb_glue`, `pkb_features`, `pkb_datapath`, `pkb_callpath`, `pkb_componentpath`, `pkb_configpath`) may still inherit; after the run, read `pkb_run_profile` from `run-catalog.json` / `run-index.json` / `summary.json` as the complete resolved list, including inherited glue/features and the integer `pkb_parallel`. Do not assume omitted keys equal project pickleball.properties. This is not a skip of Workbench; it is how you find failures. Do not start a worker just to run the whole suite. After the run, open `run-catalog.json`, then only the relevant `run-index.json` / `summary.json`. If `workbench_*` tools already exist, you may instead read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary`. Do not ingest `events.jsonl` or screenshots to find which scenarios failed. Do not `workbench_worker_start` to read the catalog. -2. **Isolate / debug a known failing scenario** — after discovery has named the trouble spots, isolate those scenarios with live Workbench. If `workbench_*` tools are already present, use the live MCP loop below. If they are absent, do not invent IDE MCP and do not start an IDE-owned stdio server. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. Do not keep using `mvn test` for isolation/debug. -3. **Confirm after isolation** — one bounded diagnostic `mvn test` with the same complete AI `pkb_runvars` (`pkb_browser=CHROME_HEADLESS`, high `pkb_parallel` when more than one scenario, `pkb_reportingmode=diagnostic`, `pkb_loglevel=warn`, `pkb_reportretention=failed`, narrowest useful `pkb_tags`/`pkb_name`). -4. **Emit the human handoff, then edit real consumer source** — write `.pickleball/investigations//` then in chat print only `.pickleball/investigations//report.html`. Change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. - -Discover must work with zero MCP. Missing `workbench_*` tools is not a reason to skip Discover, and it is not a reason to self-register IDE MCP. A multi-scenario diagnostic test is a reason to run `mvn test` first; do not start a worker to run the whole suite. +`hint` (alias `discover-hint`) prints the recommended Discover `pkb_runvars` and `NEXT: run discover`. `mcp` and `ui` are host/human commands. Agents must not use `ui`. Hosts may already wire `mcp .`; that is optional host wiring, mentioned once, not an agent setup step. Do not copy consumer features into `.pickleball` as a sandbox. ### Live isolation loop -This loop isolates a **known** failing scenario after Discover. Hosts that already run Workbench MCP may wire `tools.dscode.launcher.PickleballWorkbenchLauncher` with `classpathScope=test` and args `mcp .` (not a GUI command). That is optional host wiring, not an agent setup step. Agents must not configure, self-register, or start an IDE-owned stdio MCP process. - -From the consumer project, with Pickleball on the test classpath: +This loop isolates a **known** failing scenario after Discover. Prefer Workbench CLI `isolate`. If `workbench_*` tools are already in this session, they are the same loop (`workbench_sync` / `workbench_worker_start` must use the Discover snapshot replayed as `pkb_runvars`). -1. If `workbench_*` tools are already in this session, continue with `workbench_sync` / `workbench_worker_start`. If they are absent, do not invent IDE MCP. Stop and tell the human that live isolate needs either pre-wired `workbench_*` MCP tools or a Workbench CLI session. -2. Call `workbench_sync` once. The agent must call it; Workbench does not auto-watch. Full compile when there is no live classpath or when Java/`pom`/dependencies changed; resources-only for feature/config/data; skip when unchanged. Live buffer edits need no sync. -3. `workbench_worker_start` — reuse the compiled live classpath; do not rebuild to start a worker. -4. `workbench_request_control` -5. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. -6. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`; screenshot bytes are expensive in agent context. -7. When you need a retained evidence pack, run **one** diagnostic `mvn test` with `pkb_runvars` (below). Read that pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` instead of globbing `reports/diagnostic-runs`. -8. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. Do not paste the report body, cause/fix essays, or screenshots into the chat panel. +1. Workbench `isolate` (CLI) or, when already connected, `workbench_sync` then `workbench_worker_start`. +2. `workbench_request_control` when using the MCP alias. +3. Isolate with `workbench_execute_step` and/or `workbench_player_replace_document`. +4. Inspect with `workbench_browser_page`, `workbench_element_inspect`, and paged `workbench_events`. Prefer those over `workbench_browser_screenshot`. +5. Confirm with Workbench `confirm`. Read the pack with `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` when those tools already exist. +6. Emit the human handoff with `workbench_investigation_emit` or `DiagnosticCli emit-investigation`. In chat print only `.pickleball/investigations//report.html`. -`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Insert, nest, or retry in the same paused browser/Mapping state. MCP `isError=true` is for controller/runtime problems such as a missing paused worker; do not restart the worker merely because a step failed. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). - -Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`); they do not require Maven. +`workbench_execute_step` returns a structured `SUCCESS` / `FAILED` / `UNAVAILABLE` result. A FAILED Gherkin hypothesis does not end the worker, does not fail the paused scenario, and is not an MCP `isError`. Page `workbench_events` with `afterSequence` and a small `limit` (default 100, max 500). Live buffer edits do not require `workbench_sync` and do not write the original `.feature` until explicit Save (`workbench_request_save`). Worker restart without rebuild already exists (`workbench_worker_restart`). Step Overrides compile worker-side (`workbench_step_override_compile`). ### Generated trees are not the project @@ -54,7 +46,7 @@ Worker restart without rebuild already exists (`workbench_worker_restart`). Step Keep first-read small. After a successful export: 1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Stay in this guide's tool chooser: Discover with a diagnostic `mvn test` when the failing scenario is unknown (zero MCP is enough). Isolate a known failure with already-connected `workbench_*` tools or a Workbench CLI session; do not self-register IDE MCP. +2. Stay in this guide's tool chooser: Workbench `discover` when the failing scenario is unknown. Isolate a known failure with Workbench `isolate`, or with already-connected `workbench_*` tools as the same loop. Do not start the GUI. 3. Inspect the **real** consumer `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. 4. Open a specific exported guide only when that topic is needed, for example `docs/dynamic-steps.md`, `docs/diagnostic-reporting.md`, `docs/configuration.md`, or `docs/ai-run-configuration.md`. 5. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. @@ -146,25 +138,25 @@ Never supply `pkb_run_profile` or `pkb_run_profile.` as input. They are When you launch Pickleball tests and the intended execution settings are known, use `pkb_runvars` as the authoritative input. Put intentional tag/name selection, browser, evidence/logging controls, and other non-secret RunVar changes inside `pkb_runvars`; do not default to ambient optional project settings or separate JVM `-Dpkb_*` RunVars. Use `pkb_profile` or ordinary JVM RunVar overrides only when the task specifically tests those configuration semantics or the user asks for them. Keep protected secrets and diagnostic lineage outside `pkb_runvars`. -For an agent's bounded confirmation `mvn test` (not the human runner defaults), include diagnostic evidence controls, headless Chrome, and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars`: +For an agent's bounded confirmation (not the human runner defaults), include diagnostic evidence controls, the browser ladder (keep a remote `pkb_browser`; otherwise prefer `CHROME_HEADLESS`), and high parallelism when more than one scenario will run. Documented AI Discover/Confirm `pkb_runvars` keys: ```text -pkb_browser=CHROME_HEADLESS +pkb_browser= pkb_parallel= pkb_reportingmode=diagnostic pkb_loglevel=warn pkb_reportretention=failed ``` -Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. `DiagnosticCli discover-hint` prints the estimated integer `pkb_parallel` for the current JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. +Use the narrowest `pkb_tags` / `pkb_name` that isolate the failure. Do not add the `pretty` plugin; it is console noise for agents. `pkb_reportretention=failed` keeps dense evidence for failing scenarios and does not retain it for passing ones. Workbench `hint` prints the estimated integer `pkb_parallel` and the selected browser for the current project/JVM. `pkb_parallel=auto` also resolves to that estimate at run start and stamps the integer into `pkb_run_profile`. -These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after a live-loop isolation: +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`, often headed Chrome). Example confirmation after isolation: ```text -mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_parallel=auto, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +PickleballWorkbenchLauncher confirm --tags=@the-failing-tag --name='The failing scenario' ``` -After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. +After any diagnostic run, read `pkb_run_profile` from the pack. That is the complete resolved RunVar list, including inherited execution-context paths and the integer parallel count. Do not treat omitted `pkb_runvars` keys as equal to project `pickleball.properties`. Isolate and confirm replay that retained profile through `pkb_runvars`; they do not silently re-resolve from project defaults. A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: @@ -222,21 +214,31 @@ After isolation and the diagnostic rerun, emit a small human handoff. JSON is th ## Diagnostic utility commands -From a Maven consumer where Pickleball is on the test classpath: +The agent-facing name is Workbench. From a Maven consumer where Pickleball is on the test classpath: + +```text +PickleballWorkbenchLauncher export-guidance .pickleball +PickleballWorkbenchLauncher hint +PickleballWorkbenchLauncher discover [--tags ] [--name ] +PickleballWorkbenchLauncher isolate [--tags ] [--name ] +PickleballWorkbenchLauncher confirm [--tags ] [--name ] +``` + +`DiagnosticCli` remains the implementation behind export-guidance/hint and the comparison/rebuild utilities: ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print that DiagnosticCli list and state that Workbench is the agent entry. -Use `guidance` to print this guide, `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, and `discover-hint` for the complete diagnostic `mvn test` `pkb_runvars` one-liner (headless Chrome, estimated `pkb_parallel`, diagnostic evidence controls) plus `run-catalog.json` / `pkb_run_profile` next step. Prefer `DiagnosticCli` over constructing Maven classpaths and JShell scripts for routine diagnostic operations. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. +Use Workbench `export-guidance` to materialize the complete version-matched documentation plus curated Maven consumer reference, `hint` for the complete Discover `pkb_runvars` (browser ladder, estimated `pkb_parallel`, diagnostic evidence controls), and `discover` / `isolate` / `confirm` for the turnkey loop. `emit-investigation` writes `.pickleball/investigations//investigation.json` and `report.html` and prints the relative HTML path. ## Controlled diagnostic reruns @@ -300,14 +302,16 @@ If the consumer is nested inside the Pickleball source repository, repository-le For a normal external consumer, do not assume those core files exist. -## Maintainer pointer-eval harness +## Maintainer-only: pointer-eval harness + +This section is for Pickleball maintainers. It is not the product suite and is not first-read for consumer agents. -Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. +Pickleball's example Maven consumer includes an opt-in mixed pass/fail suite tagged only `@agent-pointer-eval` (`maven-consumer-project/src/test/resources/features/agent-pointer-eval.feature`). It is not part of `@all`, `@regression`, or the other Maven suite-profile tags. The failures are intentional canned fixtures for scoring whether a consumer AI agent follows the short `AGENTS.md` pointer into this guide and then uses Workbench discover-then-isolate. Do not treat those failures as product bugs, and do not "fix" the feature unless a human asked to change the harness. During an eval, still follow Discover / Isolate / Confirm; do not ignore canned fails. Run it explicitly: ```text -mvn test -Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS" +PickleballWorkbenchLauncher discover --tags=@agent-pointer-eval ``` -or `-Dpkb_tags=@agent-pointer-eval`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. +or `-Dpkb_runvars="pkb_tags=@agent-pointer-eval, pkb_browser=CHROME_HEADLESS"`. Do not add this tag to consumer `AGENTS.md` or Copilot pointer files. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/consumer-project.md b/src/main/resources/META-INF/pickleball/guidance/docs/consumer-project.md index befba07c..48e16392 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/consumer-project.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/consumer-project.md @@ -9,7 +9,7 @@ The nested consumer intentionally keeps its own Markdown minimal. Detailed usage From a Maven consumer with Pickleball on the test classpath: ```powershell -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.common.reporting.diagnostic.DiagnosticCli" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball" +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=export-guidance .pickleball" ``` Then read or browse: @@ -26,7 +26,7 @@ Rerun export before Pickleball work even when `.pickleball` already exists. A su Compatibility note: an older Pickleball release whose exporter predates the manifest lifecycle may leave newer files or a newer manifest behind after a downgrade. Those leftovers are not authoritative for the downgraded dependency; prefer the dependency actually resolved on the test classpath and the files freshly exported by that dependency. -AI agents should read `.pickleball/AGENT-GUIDE.md` first after a successful export. Agents discover which scenarios fail with a diagnostic `mvn test`; MCP is optional if already connected, and agents do not self-register IDE MCP. That guide's tool chooser is the agent path: one diagnostic `mvn test` to discover which scenarios fail when that is still unknown; live Workbench only when `workbench_*` tools are already available or a Workbench CLI session exists; one bounded diagnostic confirmation; then edits to the real consumer source. Do not treat `.pickleball/maven-consumer-project/` as the project under test, and do not dump `docs/README.md` or the whole snapshot into first-read context. Human readers can start with `.pickleball/docs/README.md`; links from those guides to `maven-consumer-project` resolve to the exported version-matched reference files. +AI agents should read `.pickleball/AGENT-GUIDE.md` first after a successful export. Workbench is the one front door: `discover`, `isolate`, and `confirm`. Do not start the GUI. Do not register IDE MCP. If `workbench_*` tools are already connected they are an Isolate alias. Do not treat `.pickleball/maven-consumer-project/` as the project under test, and do not dump `docs/README.md` or the whole snapshot into first-read context. Human readers can start with `.pickleball/docs/README.md`; links from those guides to `maven-consumer-project` resolve to the exported version-matched reference files. ## Version-matched reference snapshot @@ -152,10 +152,10 @@ mvn test -Dpkb_tags="@forms and @state-assertions" mvn test -Dpkb_tags="@workflow and @nested-steps and not @block-conditionals" ``` -Human `PickleballTests` defaults remain `pretty` and `@all`. Agents launching a bounded confirmation should not reuse those defaults. Use a separate `pkb_runvars` command, for example: +Human `PickleballTests` defaults remain `pretty` and `@all`. Agents launching a bounded confirmation should not reuse those defaults. Use Workbench `confirm`, or a separate `pkb_runvars` command that honors the browser ladder: ```bash -mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_parallel=auto, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +PickleballWorkbenchLauncher confirm --tags=@the-failing-tag --name='The failing scenario' ``` After the run, read `pkb_run_profile` from `run-catalog.json` / `run-index.json` / `summary.json`. That is the complete resolved RunVar list. Do not assume omitted `pkb_runvars` keys equal project `pickleball.properties`. @@ -235,7 +235,8 @@ Do not recursively ingest an entire run. ### Diagnostic CLI ```text -DiagnosticCli discover-hint +PickleballWorkbenchLauncher hint +PickleballWorkbenchLauncher discover DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli emit-investigation diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/diagnostic-reporting.md b/src/main/resources/META-INF/pickleball/guidance/docs/diagnostic-reporting.md index 449fd205..68d2c003 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/diagnostic-reporting.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/diagnostic-reporting.md @@ -114,19 +114,19 @@ See `docs/ai-run-configuration.md` and `docs/diagnostic-lineage-metadata.md`. ## Diagnostic CLI -Supported command-line operations: +The agent-facing entry is Pickleball Workbench. DiagnosticCli remains the implementation behind export-guidance/hint and the comparison utilities: ```text DiagnosticCli guidance DiagnosticCli export-guidance [output-directory] -DiagnosticCli discover-hint +DiagnosticCli discover-hint [project] DiagnosticCli emit-investigation DiagnosticCli compare-runs [output-json] DiagnosticCli compare-fingerprints [output-json] DiagnosticCli rebuild ``` -`DiagnosticCli help`, `--help`, and `-h` print this same command list. +`DiagnosticCli help`, `--help`, and `-h` print this list and state that Workbench is the agent entry. Prefer `DiagnosticCli` over custom Maven-classpath/JShell workflows for routine comparison and recovery. diff --git a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md index 128dea62..459aba95 100644 --- a/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md +++ b/src/main/resources/META-INF/pickleball/guidance/docs/pickleball-workbench.md @@ -45,7 +45,23 @@ META-INF/pickleball/workbench/pickleball-workbench.jar Run the small launcher from the consumer test classpath. For Maven consumers, this command requires no cache path, separate Workbench dependency, or separately selected version. -Hosts that already run Workbench MCP can launch it from the consumer test classpath: +Workbench is the consumer AI-agent front door. Agents use `export-guidance`, `hint`, `discover`, `isolate`, and `confirm`. Do not start the GUI. Do not register IDE MCP. + +```bash +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ + -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ + -Dexec.classpathScope=test \ + "-Dexec.args=export-guidance .pickleball" +``` + +```bash +"-Dexec.args=hint" +"-Dexec.args=discover --tags=@smoke" +"-Dexec.args=isolate --name='The failing scenario'" +"-Dexec.args=confirm --tags=@smoke" +``` + +`mcp` and `ui` remain host/human commands. Hosts that already run Workbench MCP can launch it from the consumer test classpath: ```bash mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ @@ -58,7 +74,7 @@ mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -Humans who want the Swing player can pass `ui .` instead. With no launcher arguments, `ui` and the current directory are selected automatically for that human default. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."`. Agents discover unknown failures with a diagnostic `mvn test`. Headless `mcp .` is optional host wiring when `workbench_*` tools are already connected; agents must not self-register IDE MCP. Agents for this release should not use the GUI, `ui .`, or `.pickleball/workbench/attach.json` as their path; see `.pickleball/AGENT-GUIDE.md`. +Humans who want the Swing player can pass `ui .` instead. With no launcher arguments, `ui` and the current directory are selected automatically for that human default. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."`. Headless `mcp .` is optional host wiring when `workbench_*` tools are already connected; agents must not self-register IDE MCP. Agents for this release should not use the GUI, `ui .`, or `.pickleball/workbench/attach.json` as their path; see `.pickleball/AGENT-GUIDE.md`. Gradle consumers can expose the same dependency-owned launcher without resolving a cache path or adding a Workbench dependency: @@ -321,7 +337,7 @@ Start the lightweight non-Spring MCP server for a consumer project. This is opti java -jar $workbenchJar mcp ".\maven-consumer-project" ``` -Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. That launcher is optional host wiring for environments that already run MCP. Agents discover unknown failures with a diagnostic `mvn test` and `run-catalog.json` (zero MCP is enough). They must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with already-connected `workbench_*` tools or a Workbench CLI session. Do not document or use the Swing GUI, `ui .`, or `.pickleball/workbench/attach.json` as the agent path. +Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. That launcher is optional host wiring for environments that already run MCP. Agents use Workbench `discover` / `isolate` / `confirm`. They must not self-register IDE MCP or start an IDE-owned stdio server. Isolate a known failure with Workbench CLI `isolate` or already-connected `workbench_*` tools. Do not document or use the Swing GUI, `ui .`, or `.pickleball/workbench/attach.json` as the agent path. The server uses the official Java MCP SDK core and stdio transport with the Jackson 2 JSON adapter. MCP dependencies are Workbench-only and are shaded into the executable companion. Workbench deliberately does not use Spring Boot, Spring Framework, Spring AI, WebMVC, or Tomcat. diff --git a/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderTest.java b/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderTest.java new file mode 100644 index 00000000..eec36582 --- /dev/null +++ b/src/test/java/tools/dscode/common/reporting/diagnostic/AgentBrowserLadderTest.java @@ -0,0 +1,61 @@ +package tools.dscode.common.reporting.diagnostic; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentBrowserLadderTest { + @TempDir + Path tempDir; + + @Test + void remoteProjectBrowserIsKept() throws Exception { + writeProperties("pkb_browser=SAUCE_CHROME\n"); + Files.createDirectories(tempDir.resolve("src/test/resources/configs")); + Files.writeString(tempDir.resolve("src/test/resources/configs/GRID_CHROME.yaml"), "unused: true\n"); + + AgentBrowserLadder.Decision decision = AgentBrowserLadder.select(tempDir); + + assertEquals("SAUCE_CHROME", decision.browser()); + assertTrue(decision.remoteKept()); + assertTrue(AgentBrowserLadder.isRemoteBrowser("GRID_EDGE")); + assertTrue(AgentBrowserLadder.isRemoteBrowser("REMOTE_CHROME")); + } + + @Test + void localBrowserPrefersChromeHeadlessAndIgnoresUnusedSauceYaml() throws Exception { + writeProperties("pkb_browser=chrome\n"); + Path configs = tempDir.resolve("src/test/resources/configs"); + Files.createDirectories(configs); + Files.writeString(configs.resolve("SAUCE_CHROME.yaml"), "unused: true\n"); + Files.writeString(configs.resolve("GRID_CHROME.yaml"), "unused: true\n"); + Files.writeString(configs.resolve("REMOTE_EDGE.yaml"), "unused: true\n"); + + AgentBrowserLadder.Decision decision = AgentBrowserLadder.select(tempDir); + + assertEquals("CHROME_HEADLESS", decision.browser()); + assertFalse(decision.remoteKept()); + assertEquals("chrome", decision.projectBrowser()); + assertNull(AgentBrowserLadder.fallbackIfHeadlessCannotStart(decision)); + } + + @Test + void missingBrowserPrefersBundledChromeHeadless() { + AgentBrowserLadder.Decision decision = AgentBrowserLadder.select(tempDir); + assertEquals("CHROME_HEADLESS", decision.browser()); + assertFalse(decision.remoteKept()); + } + + private void writeProperties(String body) throws Exception { + Path resources = tempDir.resolve("src/test/resources"); + Files.createDirectories(resources); + Files.writeString(resources.resolve("pickleball.properties"), body); + } +} diff --git a/src/test/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlannerTest.java b/src/test/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlannerTest.java new file mode 100644 index 00000000..d6162fe1 --- /dev/null +++ b/src/test/java/tools/dscode/common/reporting/diagnostic/AgentDiscoverPlannerTest.java @@ -0,0 +1,82 @@ +package tools.dscode.common.reporting.diagnostic; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import tools.dscode.parallelutilities.ParallelCountEstimator; +import tools.dscode.testengine.PKB_props; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AgentDiscoverPlannerTest { + @TempDir + Path tempDir; + + @Test + void discoverRunVarsUseBrowserLadderAndEstimatedParallel() throws Exception { + Path resources = tempDir.resolve("src/test/resources"); + Files.createDirectories(resources); + Files.writeString(resources.resolve("pickleball.properties"), "pkb_browser=SAUCE_CHROME\n"); + + AgentDiscoverPlanner.Plan plan = AgentDiscoverPlanner.discover(tempDir, "@smoke", null); + + assertTrue(plan.runVars().contains("pkb_browser=SAUCE_CHROME")); + assertTrue(plan.runVars().contains("pkb_parallel=" + ParallelCountEstimator.estimate())); + assertTrue(plan.runVars().contains("pkb_reportingmode=diagnostic")); + assertTrue(plan.runVars().contains("pkb_tags=@smoke")); + assertFalse(plan.runVars().contains("pkb_parallel=80")); + assertFalse(plan.runVars().contains("pkb_run_profile=")); + } + + @Test + void isolateReplaysRetainedProfileWithoutReinheritingDefaults() { + LinkedHashMap retained = new LinkedHashMap<>(); + retained.put(PKB_props.PKB_BROWSER, "CHROME_HEADLESS"); + retained.put(PKB_props.PKB_PARALLEL, "12"); + retained.put(PKB_props.PKB_GLUE, "com.example.pickleball"); + retained.put(PKB_props.PKB_REPORTING_MODE, "diagnostic"); + retained.put(PKB_props.PKB_PLUGINS, ""); + + String isolate = AgentDiscoverPlanner.isolateRunVars(retained, "@failing", "Broken scenario"); + + assertTrue(isolate.contains("pkb_browser=CHROME_HEADLESS")); + assertTrue(isolate.contains("pkb_parallel=1")); + assertTrue(isolate.contains("pkb_glue=com.example.pickleball")); + assertTrue(isolate.contains("pkb_tags=@failing")); + assertTrue(isolate.contains("pkb_name=")); + assertFalse(isolate.contains("pkb_parallel=12")); + assertFalse(isolate.contains("pkb_run_profile=")); + assertFalse(isolate.contains("pretty")); + assertFalse(isolate.contains("@all")); + } + + @Test + void confirmReplaysSnapshotAndOverlaysSelection() { + Map retained = Map.of( + PKB_props.PKB_BROWSER, "GRID_CHROME", + PKB_props.PKB_PARALLEL, "8", + PKB_props.PKB_REPORTING_MODE, "diagnostic" + ); + String confirm = AgentDiscoverPlanner.confirmRunVars(retained, "@one", null); + assertTrue(confirm.contains("pkb_browser=GRID_CHROME")); + assertTrue(confirm.contains("pkb_parallel=8")); + assertTrue(confirm.contains("pkb_tags=@one")); + } + + @Test + void missingSnapshotFailsClearly() { + IllegalStateException failure = assertThrows( + IllegalStateException.class, + () -> AgentDiscoverPlanner.isolateRunVars(Map.of(), null, null) + ); + assertTrue(failure.getMessage().contains("No prior Discover snapshot")); + assertFalse(failure.getMessage().toLowerCase().contains("mcp")); + } +} diff --git a/src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java b/src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java index 929b4043..7d6f952b 100644 --- a/src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java +++ b/src/test/java/tools/dscode/launcher/PickleballWorkbenchLauncherTest.java @@ -49,10 +49,42 @@ void extractionIsContentAddressedAndCommandAlwaysStartsASeparateJarProcess() thr String[] implicitProject = PickleballWorkbenchLauncher.normalizedArguments( new String[]{"mcp"} ); + String[] isolate = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"isolate", "--tags=@failing"} + ); + String[] discoverHint = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"hint"} + ); assertEquals("ui", defaults[0]); assertEquals(Path.of("").toAbsolutePath().normalize().toString(), defaults[1]); assertEquals("mcp", implicitProject[0]); assertEquals(Path.of("").toAbsolutePath().normalize().toString(), implicitProject[1]); + assertEquals("isolate", isolate[0]); + assertEquals(Path.of("").toAbsolutePath().normalize().toString(), isolate[1]); + assertEquals("--tags", isolate[2]); + assertEquals("@failing", isolate[3]); + assertEquals("hint", discoverHint[0]); + + String[] exportGuidance = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"export-guidance", ".pickleball"} + ); + assertEquals("export-guidance", exportGuidance[0]); + assertEquals(".pickleball", exportGuidance[1]); + + String[] isolateName = PickleballWorkbenchLauncher.normalizedArguments( + new String[]{"isolate", "--name=The", "failing", "scenario"} + ); + assertEquals("isolate", isolateName[0]); + assertEquals(Path.of("").toAbsolutePath().normalize().toString(), isolateName[1]); + assertEquals("--name", isolateName[2]); + assertEquals("The failing scenario", isolateName[3]); + + WorkbenchCommandLine.Parsed confirm = WorkbenchCommandLine.parse( + new String[]{"confirm", "--name=Agent", "pointer", "eval", "failing", "fruit", "mismatch"} + ); + assertEquals("confirm", confirm.command()); + assertEquals("Agent pointer eval failing fruit mismatch", confirm.name()); + assertEquals(Path.of("").toAbsolutePath().normalize(), confirm.project()); byte[] streamed = {9, 8, 7, 6}; Path fromStream = PickleballWorkbenchLauncher.extractPayload( diff --git a/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandsTest.java b/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandsTest.java new file mode 100644 index 00000000..cac4f284 --- /dev/null +++ b/src/test/java/tools/dscode/launcher/WorkbenchAgentCommandsTest.java @@ -0,0 +1,114 @@ +package tools.dscode.launcher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import tools.dscode.common.reporting.diagnostic.LastDiscoverSnapshot; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WorkbenchAgentCommandsTest { + @TempDir + Path tempDir; + + @Test + void hintPrintsLadderRunVarsAndNextDiscover() throws Exception { + Path resources = tempDir.resolve("src/test/resources"); + Files.createDirectories(resources); + Files.writeString(resources.resolve("pickleball.properties"), "pkb_browser=chrome\n"); + + Output output = run("hint", tempDir.toString()); + + assertEquals(0, output.exitCode()); + assertTrue(output.stdout().contains("pkb_browser=CHROME_HEADLESS")); + assertTrue(output.stdout().contains("NEXT: run discover")); + assertFalse(output.stdout().contains("MUST")); + assertFalse(output.stdout().contains("pkb_parallel=80")); + } + + @Test + void exportGuidanceUsesDiagnosticCli() throws Exception { + Path outputDir = tempDir.resolve("guidance"); + Output output = run("export-guidance", outputDir.toString()); + assertEquals(0, output.exitCode()); + assertTrue(Files.isRegularFile(outputDir.resolve("AGENT-GUIDE.md"))); + assertTrue(output.stdout().contains("NEXT: follow AGENT-GUIDE")); + assertTrue(output.stdout().contains("Workbench discover")); + } + + @Test + void discoverWrapsMavenAndRecordsSnapshot() throws Exception { + Path catalogDir = tempDir.resolve("reports/diagnostic-runs"); + Files.createDirectories(catalogDir); + List> captured = new ArrayList<>(); + int exit = WorkbenchAgentCommands.run( + new String[]{"discover", tempDir.toString(), "--tags=@smoke"}, + System.out, + System.err, + (project, command, out, err) -> { + captured.add(command); + try { + Files.writeString(catalogDir.resolve("run-catalog.json"), """ + { + "schemaVersion": 1, + "runs": [ + { + "runId": "run-1", + "runProfile": "pkb_browser=CHROME_HEADLESS, pkb_parallel=4, pkb_reportingmode=diagnostic, pkb_tags=@smoke", + "lineage": { "runPurpose": "workbench-discover" } + } + ] + } + """); + } catch (Exception failure) { + throw new RuntimeException(failure); + } + return 1; + } + ); + + assertEquals(1, exit); + assertEquals(1, captured.size()); + assertTrue(captured.getFirst().stream().anyMatch(item -> item.startsWith("-Dpkb_runvars="))); + assertTrue(captured.getFirst().contains("-Dpkb_run_purpose=workbench-discover")); + LastDiscoverSnapshot.Snapshot snapshot = LastDiscoverSnapshot.read(tempDir); + assertTrue(snapshot.hasRunVars()); + assertTrue(snapshot.runProfile().contains("pkb_browser=CHROME_HEADLESS")); + } + + @Test + void confirmRequiresDiscoverSnapshot() { + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + int exit = WorkbenchAgentCommands.run( + new String[]{"confirm", tempDir.toString()}, + System.out, + new PrintStream(stderr, true, StandardCharsets.UTF_8) + ); + assertEquals(1, exit); + String errors = stderr.toString(StandardCharsets.UTF_8); + assertTrue(errors.contains("No prior Discover snapshot")); + assertFalse(errors.toLowerCase().contains("register")); + } + + private Output run(String... args) { + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + int exit = WorkbenchAgentCommands.run( + args, + new PrintStream(stdout, true, StandardCharsets.UTF_8), + new PrintStream(stderr, true, StandardCharsets.UTF_8) + ); + return new Output(exit, stdout.toString(StandardCharsets.UTF_8), stderr.toString(StandardCharsets.UTF_8)); + } + + private record Output(int exitCode, String stdout, String stderr) { } +}