From e8f965fcb86d01713b708f44cdabe98411ccc5f9 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 25 Aug 2026 04:05:03 +0000 Subject: [PATCH] Teach consumer agents the live MCP loop and skip unchanged Workbench compiles. Put a headless MCP tool chooser first in AGENT-GUIDE, skip Maven/Gradle when Java did not change, refresh resources without test-compile, and add sparse diagnostic catalog/run/summary MCP tools. Co-authored-by: ds-code-t --- docs/README.md | 2 +- docs/agent/feature-map.md | 6 +- docs/agent/repository-index.md | 4 + docs/ai-run-configuration.md | 6 + docs/consumer-agent-guide.md | 87 +++++-- docs/consumer-project.md | 22 +- docs/pickleball-workbench.md | 51 ++-- .../diagnostic/PickleballGuidanceChecks.java | 17 +- pickleball-workbench/AGENTS.md | 6 +- .../dscode/workbench/WorkbenchController.java | 18 ++ .../dscode/workbench/WorkbenchServices.java | 6 + .../DiagnosticEvidenceNavigator.java | 60 +++++ .../workbench/mcp/WorkbenchMcpTools.java | 16 ++ .../workbench/sync/WorkbenchManifest.java | 66 ++++- .../workbench/sync/WorkbenchSyncInputs.java | 239 ++++++++++++++++++ .../workbench/sync/WorkbenchSyncMode.java | 11 + .../workbench/sync/WorkbenchSyncPlanner.java | 34 +++ .../workbench/sync/WorkbenchSynchronizer.java | 140 +++++++++- .../catalog/ConsumerFeatureCatalogTest.java | 8 +- .../DiagnosticEvidenceNavigatorTest.java | 39 +++ .../workbench/mcp/WorkbenchMcpServerTest.java | 41 +++ .../sync/WorkbenchSyncPlannerTest.java | 90 +++++++ .../sync/WorkbenchSynchronizerTest.java | 198 +++++++++++++++ .../ui/WorkbenchUiControllerTest.java | 8 +- .../worker/WorkbenchWorkerManagerTest.java | 8 +- .../pickleball/guidance/AGENT-GUIDE.md | 87 +++++-- .../pickleball/guidance/docs/README.md | 2 +- .../guidance/docs/agent/feature-map.md | 6 +- .../guidance/docs/agent/repository-index.md | 4 + .../guidance/docs/ai-run-configuration.md | 6 + .../guidance/docs/consumer-agent-guide.md | 87 +++++-- .../guidance/docs/consumer-project.md | 22 +- .../guidance/docs/pickleball-workbench.md | 51 ++-- 33 files changed, 1307 insertions(+), 141 deletions(-) create mode 100644 pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java create mode 100644 pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java create mode 100644 pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java create mode 100644 pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java diff --git a/docs/README.md b/docs/README.md index 6f1ae4cd..3d8d3cb7 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 and AI agents can therefore inspect the same working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. +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. ## Start here diff --git a/docs/agent/feature-map.md b/docs/agent/feature-map.md index 8fcff1d1..e4121374 100644 --- a/docs/agent/feature-map.md +++ b/docs/agent/feature-map.md @@ -6,7 +6,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab |---|---|---|---| | Build, publication, Java compatibility, nested controller distribution | `build.gradle`; `settings.gradle`; `pickleball-control-protocol/build.gradle`; `pickleball-workbench/build.gradle`; `src/main/aspectj`; search `verifyStrictControllerIsolation`, `verifyEmbeddedWorkbench`, `shadowJar`, `JavaLanguageVersion` | root tests; protocol `check`; `:pickleball-workbench:test`; artifact/dependency verification; Maven consumer build | `README.md`; `docs/getting-started.md`; `docs/cucumber-compatibility.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` | | Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records | `verifyProtocolIsolation`; `ControlBridgeClientTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.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`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `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`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.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`; `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`; `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` | | 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` | @@ -41,12 +41,12 @@ The standalone Workbench shadow JAR is controller-only and self-contained, with The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`, `PKB_CONTROL_BRIDGE_SESSION_ID`, `PKB_CONTROL_BRIDGE_TOKEN`, and `PKB_CONTROL_BRIDGE_PAUSE_FIRST_SCENARIO`. Pickleball may also accept the prior `PKB_STUDIO_BRIDGE_*` names as deprecated compatibility input aliases. New Workbench code must use only the neutral names. -`WorkbenchSynchronizer` uses the selected Maven/Gradle wrapper to run the minimum test-compilation/resource lifecycle and capture the effective test runtime dependency classpath; Gradle metadata is obtained with a temporary init script rather than the Gradle Tooling API. It materializes `.pickleball/workbench/base/classes` as immutable synchronization provenance and one merged `.pickleball/workbench/live/classes` runtime root, applying main output first and test output second so test-owned paths win deterministically. `base` is never placed on `classpath.txt` or a worker classpath. The synchronization fingerprint includes dependency artifact contents as well as merged project output. +`WorkbenchSynchronizer` uses the selected Maven/Gradle wrapper to run the minimum test-compilation/resource lifecycle and capture the effective test runtime dependency classpath; Gradle metadata is obtained with a temporary init script rather than the Gradle Tooling API. Input fingerprints of Java sources, resources, build files, and dependency artifacts decide skip vs resources-only vs full compile; the output fingerprint in `manifest.json` remains provenance, not a skip key. Sync always passes `-DskipTests`. It materializes `.pickleball/workbench/base/classes` as immutable synchronization provenance and one merged `.pickleball/workbench/live/classes` runtime root, applying main output first and test output second so test-owned paths win deterministically. `base` is never placed on `classpath.txt` or a worker classpath. The synchronization fingerprint includes dependency artifact contents as well as merged project output. Live Gherkin buffer edits do not require sync. `WorkbenchWorkerManager` launches a consumer JVM directly from that live root plus captured dependencies through the protocol-owned worker class-name contract, without linking the worker class or invoking Maven/Gradle. Interactive workers use a session-private anchor feature and a one-shot `BEFORE_STEP` breakpoint to reach an initialized, paused marker before the controller returns a live worker. Pause leases remain finite and are renewed while the controller owns the anchor. `WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. 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). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. 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 headless MCP (`mcp .`). 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 163dbf16..f44d9c18 100644 --- a/docs/agent/repository-index.md +++ b/docs/agent/repository-index.md @@ -505,6 +505,9 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchProject.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/terminal/WorkerLogBuffer.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/terminal/WorkerLogFiles.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java` @@ -550,6 +553,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveScenarioPlayerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchGradleSynchronizerIntegrationTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java` +- `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/terminal/WorkerLogBufferTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java` diff --git a/docs/ai-run-configuration.md b/docs/ai-run-configuration.md index 97ef5401..109a9fda 100644 --- a/docs/ai-run-configuration.md +++ b/docs/ai-run-configuration.md @@ -243,6 +243,12 @@ Example compact rerun: -Dpkb_changed_variables=pkb_browser ``` +For an agent's bounded confirmation `mvn test` (not `PickleballTests` human defaults of `pretty` / `@all`), include diagnostic evidence controls and keep selection narrow: + +```text +-Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + Lineage metadata is not execution configuration: ```text diff --git a/docs/consumer-agent-guide.md b/docs/consumer-agent-guide.md index 9160a940..ac261637 100644 --- a/docs/consumer-agent-guide.md +++ b/docs/consumer-agent-guide.md @@ -2,7 +2,53 @@ 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/`. +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. + +## Tool chooser + +Use this order. Consumer AI agents for this Pickleball release use headless Workbench MCP (`mcp .`). Do not start the Workbench GUI. + +1. **Live headless MCP** — isolate a failing step in a paused worker. Reuse compilation, rewrite Gherkin in the live buffer, and inspect the page and semantic events in the same browser/Mapping state. +2. **One diagnostic `mvn test`** — after the live loop has isolated the failure, run one bounded confirmation with `pkb_runvars` so an evidence pack is retained. +3. **Edit real consumer source** — change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. + +Do not copy consumer features into `.pickleball` as a sandbox. + +### Live isolation loop + +From the consumer project, with Pickleball on the test classpath: + +1. Start the launcher with `mcp .` (not a GUI command). +2. Call `workbench_sync` once. The synchronizer skips Maven/Gradle when Java/build/dependencies are unchanged, and refreshes test resources without a full `test-compile` when only features/config/data changed. +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 `workbench_events`. +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`. + +`workbench_execute_step` failure does not end the worker. Insert, nest, or retry in the same paused browser/Mapping state. 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. + +### Generated trees are not the project + +- `.pickleball/maven-consumer-project/` is a version-matched **read-only** reference snapshot of Pickleball's own example consumer. Do not copy, edit, or execute it as the project under test. +- `.pickleball/workbench/live/classes` is the compiled overlay for the worker classpath. Do not use it as an editor. +- `export-guidance` does **not** copy this consumer's own features into `.pickleball` for testing. It still materializes full `docs/` plus the example-consumer snapshot for on-demand/human use. + +## First-read + +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 and live loop. +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. + +Do not read `docs/README.md`, the whole `maven-consumer-project/` snapshot, or Workbench GUI pages as first actions. Those remain available on demand. + +The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. ## Generated guidance lifecycle @@ -19,26 +65,13 @@ The exporter does not create/commit a new `.gitignore`, alter the Git index, or Compatibility note: an older Pickleball release whose exporter predates the manifest lifecycle may leave newer files behind after a downgrade. Those leftovers are not authoritative for the downgraded dependency. Prefer the dependency actually resolved on the test classpath and files freshly exported by that dependency. -## First actions - -For Pickleball scenario authoring, configuration, execution, diagnostics, or troubleshooting: - -1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Read this guide before changing Pickleball scenarios or diagnosing a Pickleball run. -3. Use `docs/README.md` as the documentation map. -4. Inspect the consumer project's `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. -5. Use `maven-consumer-project/` as a version-matched read-only reference when a documented syntax/configuration example or working Pickleball consumer structure is useful. -6. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. - -The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. - ## Generated Maven consumer reference -`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. +`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. It is not the consumer project under test and is not a writable sandbox. The snapshot intentionally includes the consumer `pom.xml`, Pickleball runner, local browser/service test server, executable feature files, service-call definitions, configuration/data fixtures, local test-site resources, and the committed shared/local profile and property examples. It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` bridge, internal Java verification classes, and maintainer-only `_local2` files. -Use the snapshot to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not modify or execute files under `.pickleball/maven-consumer-project/` as the consumer project's implementation. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. +Use the snapshot only to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not copy, modify, or execute files under `.pickleball/maven-consumer-project/` as the project under test. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. `export-guidance` does not copy the consumer's own features into `.pickleball` for testing. ## Scenario authoring and fixes @@ -100,6 +133,22 @@ 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 and keep the selection narrow. Documented AI defaults: + +```text +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. + +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`). Example confirmation after a live-loop isolation: + +```text +mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: ```text @@ -139,6 +188,8 @@ Use this escalation order: Stop reading as soon as the current layer answers the investigation. Do not recursively ingest an entire diagnostic run. +From headless Workbench MCP, use `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` for layers 1–3 instead of globbing `reports/diagnostic-runs`. Those tools return sparse JSON only and do not dump `events.jsonl`, traces, or screenshot bytes. + ## Visual evidence rules - Never open a PNG merely to determine whether two screenshots differ. @@ -198,7 +249,7 @@ See `docs/ai-run-configuration.md` for the full profile/RunVar contract and `doc ## Pickleball syntax documentation -The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Use `docs/README.md` to select the relevant guide. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: +The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Open a specific guide when the live loop or a diagnostic layer requires that topic; do not start by reading `docs/README.md` as a dump. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: - dynamic Gherkin/action/assertion syntax — `docs/dynamic-steps.md`; - element vocabulary/selectors — `docs/custom-element-definitions.md`; @@ -217,7 +268,7 @@ Do not guess Pickleball syntax when the version-matched guide or executable cons ## Human-readable consumer guidance -Use `docs/consumer-project.md` for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Use `docs/README.md` to navigate the complete bundled documentation. Human readers can open files under `maven-consumer-project/` directly in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples linked from those guides. +Use `docs/consumer-project.md` on demand for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Human readers can start with `docs/README.md` and open files under `maven-consumer-project/` in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples. Agents should not treat those as first-read. ## When the core Pickleball repository is also present diff --git a/docs/consumer-project.md b/docs/consumer-project.md index 4633e07b..ace01d46 100644 --- a/docs/consumer-project.md +++ b/docs/consumer-project.md @@ -26,11 +26,11 @@ 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. 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. That guide's tool chooser is the agent path: headless Workbench MCP (`mcp .`), one bounded diagnostic `mvn test`, 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. ## Version-matched reference snapshot -`export-guidance` also materializes a curated, read-only snapshot of the canonical Pickleball Maven consumer under `.pickleball/maven-consumer-project/`. It is intended for both human readers and AI agents that need concrete working examples in addition to prose documentation. +`export-guidance` also materializes a curated, read-only snapshot of the canonical Pickleball Maven consumer under `.pickleball/maven-consumer-project/`. It is a version-matched **reference** of Pickleball's own example consumer for on-demand lookup, not a sandbox and not the consumer project under test. `export-guidance` does not copy the current consumer's own features into `.pickleball` for testing. The snapshot includes: @@ -42,7 +42,7 @@ The snapshot includes: - static local test-site resources; and - the committed shared/local `profiles*.yaml` and `pickleball*.properties` examples. -It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` and `.github/copilot-instructions.md` bridges, internal Java verification classes, and maintainer-only `_local2` files. It is reference material, not another consumer project to edit or run. Make changes in the real consumer project; a future guidance export may replace every managed file in this snapshot. +It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` and `.github/copilot-instructions.md` bridges, internal Java verification classes, and maintainer-only `_local2` files. It is reference material, not another consumer project to copy, edit, or run. Make changes in the real consumer project; a future guidance export may replace every managed file in this snapshot. `.pickleball/workbench/live/classes` is a compiled worker overlay, not an editor. ## Purpose @@ -77,20 +77,20 @@ or use the included wrappers: ## Launch the dependency-matched Workbench -The test-scoped Pickleball dependency already contains its controller-only Workbench payload. Start the launcher from the resolved test classpath without finding a Maven cache entry or declaring a second version: +The test-scoped Pickleball dependency already contains its controller-only Workbench payload. Consumer AI agents start the **headless MCP** launcher from the resolved test classpath: ```bash ./mvnw -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ -Dexec.classpathScope=test \ - "-Dexec.args=ui ." + "-Dexec.args=mcp ." ``` ```powershell -.\mvnw.cmd -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=ui ." +.\mvnw.cmd -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -The launcher verifies and extracts the opaque payload beneath `.pickleball/workbench/controller//`, then creates a separate Workbench JVM. Workbench captures this project's compiled outputs and effective test runtime before creating a separate worker JVM. Only the worker loads the consumer-resolved Pickleball runtime; the Workbench artifact and process contain no core implementation. UI and MCP both use the same controller/live session. See `docs/pickleball-workbench.md` for commands, lifecycle, protocol compatibility, and isolation checks. +Humans who want the Swing player can pass `ui .` instead. Agents for this release should not use the GUI, `ui .`, or `attach.json` as their path. The launcher verifies and extracts the opaque payload beneath `.pickleball/workbench/controller//`, then creates a separate Workbench JVM. Workbench captures this project's compiled outputs and effective test runtime before creating a separate worker JVM. Only the worker loads the consumer-resolved Pickleball runtime; the Workbench artifact and process contain no core implementation. See `docs/pickleball-workbench.md` for commands, lifecycle, protocol compatibility, and isolation checks. The live-loop order lives in `.pickleball/AGENT-GUIDE.md`. Runner defaults include: @@ -152,6 +152,12 @@ 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: + +```bash +mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + The consumer `pom.xml` also defines Maven profiles such as: ```bash @@ -260,4 +266,4 @@ When evidence supports a bounded rerun: - Port `8765` must be available for the example test server. - Nested README/AGENTS files are minimal adapters; detailed guidance and version-matched reference examples are owned by Pickleball core and exported from the dependency. -Use `docs/README.md` for the complete version-matched Pickleball syntax/documentation map and `maven-consumer-project/` for the corresponding working reference files. +Human readers can use `docs/README.md` for the complete version-matched Pickleball syntax/documentation map and `maven-consumer-project/` for the corresponding working reference files. Agents should not treat those as first-read. diff --git a/docs/pickleball-workbench.md b/docs/pickleball-workbench.md index 8df90b18..950c8f65 100644 --- a/docs/pickleball-workbench.md +++ b/docs/pickleball-workbench.md @@ -43,20 +43,22 @@ The normal `tools.dscode:pickleball:` test dependency already carries t 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: +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. + +Consumer AI agents use headless 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=ui ." + "-Dexec.args=mcp ." ``` ```powershell -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=ui ." +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -With no launcher arguments, `ui` and the current directory are selected automatically. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."` or `"-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 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: @@ -64,7 +66,7 @@ Gradle consumers can expose the same dependency-owned launcher without resolving tasks.register('pickleballWorkbench', JavaExec) { classpath = sourceSets.test.runtimeClasspath mainClass = 'tools.dscode.launcher.PickleballWorkbenchLauncher' - args 'ui', projectDir.absolutePath + args 'mcp', projectDir.absolutePath // consumer agents; pass 'ui' for the Swing player } ``` @@ -99,7 +101,15 @@ $workbenchJar = ".\pickleball-workbench\build\libs\pickleball-workbench-.jar mcp ` stays stdio JSON-RPC only. That client may hold the lease without a banner. Save is still a distinct explicit tool and never an implicit write. - -From `maven-consumer-project`: - -```bash -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ - -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ - -Dexec.classpathScope=test \ - "-Dexec.args=ui ." -``` +Headless `java -jar pickleball-workbench-.jar mcp ` stays stdio JSON-RPC only. That is the consumer-agent path. That client may hold the lease without a banner. Save is still a distinct explicit tool and never an implicit write. -Then point the agent at `.pickleball/workbench/attach.json`. Do not launch a second `mcp` process against the same live UI session. +A human-watched UI session is optional and separate. From `maven-consumer-project`, a person may start `ui .` and then a watcher can join `.pickleball/workbench/attach.json`. Do not launch a second `mcp` process against the same live UI session, and do not treat that attach file as the default agent path. ## MCP stdio -Start the lightweight non-Spring MCP server for a synchronized consumer project: +Start the lightweight non-Spring MCP server for a consumer project. This is the consumer-agent path: ```powershell java -jar $workbenchJar mcp ".\maven-consumer-project" ``` +Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. Do not document or use the Swing GUI 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. ### Stdout contract @@ -345,6 +350,8 @@ workbench_worker_stop workbench_worker_status ``` +`workbench_sync` uses the skip / resources-only / full rules above. Live buffer edits do not require it. + Live runtime, Mapping, and watched-agent control: ```text @@ -391,6 +398,14 @@ workbench_step_override_remove workbench_step_override_clear ``` +Sparse diagnostic readers (do not glob `reports/diagnostic-runs`; these return JSON only and do not dump events, traces, or PNG bytes): + +```text +workbench_diagnostic_catalog +workbench_diagnostic_run +workbench_diagnostic_summary +``` + `workbench_step_override_compile` sends the Java source template to the consumer worker. The source must contain `{{CLASS_NAME}}`; worker-side Pickleball remains responsible for compilation, generated classloaders, matching, replacement, captures, and execution. Mutating live tools require the agent control lease. `workbench_request_save` never writes the original feature until the human Allows it in the UI, or until the explicit stdio tool call itself is the headless approval. Deny, Take control, and an unsavable demo buffer leave the file unchanged. 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 be4a697d..956968a3 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 @@ -40,8 +40,17 @@ void dependencyPrintsCanonicalAgentGuide() { assertTrue(guide.contains("GUIDANCE-MANIFEST.json")); assertTrue(guide.contains("keep terminal logging minimal")); assertTrue(guide.contains("older Pickleball release whose exporter predates the manifest lifecycle")); - assertTrue(guide.contains("Generated Maven consumer reference")); - assertTrue(guide.contains("maven-consumer-project/")); + assertTrue(guide.contains("Generated Maven consumer reference")); + assertTrue(guide.contains("maven-consumer-project/")); + assertTrue(guide.contains("mcp .")); + assertTrue(guide.contains("workbench_sync")); + assertTrue(guide.contains("workbench_execute_step")); + assertTrue(guide.contains("workbench_diagnostic_catalog")); + assertTrue(guide.contains("pkb_reportingmode=diagnostic")); + assertTrue(guide.contains("pkb_reportretention=failed")); + String chooser = guide.substring(0, guide.indexOf("Generated guidance lifecycle")); + assertFalse(chooser.contains("attach.json")); + assertFalse(chooser.contains("ui .")); } @Test @@ -125,6 +134,10 @@ void dependencyExportsVersionMatchedGuidanceAndManifest() throws Exception { assertTrue(guide.contains("keep terminal logging minimal")); assertTrue(guide.contains("older Pickleball release whose exporter predates the manifest lifecycle")); assertTrue(guide.contains("read-only reference snapshot")); + assertTrue(guide.contains("mcp .")); + assertTrue(guide.contains("workbench_diagnostic_catalog")); + assertTrue(guide.contains("pkb_reportretention=failed")); + assertTrue(guide.contains("Do not copy, modify, or execute files")); String consumerProject = Files.readString(root.resolve("docs/consumer-project.md")); assertTrue(consumerProject.contains("keep console verbosity low")); diff --git a/pickleball-workbench/AGENTS.md b/pickleball-workbench/AGENTS.md index 201ad4bf..718c5e0a 100644 --- a/pickleball-workbench/AGENTS.md +++ b/pickleball-workbench/AGENTS.md @@ -103,7 +103,7 @@ Workbench provides: java -jar pickleball-workbench-.jar mcp ``` -The MCP adapter is `tools.dscode.workbench.mcp.WorkbenchMcpServer` plus `WorkbenchMcpTools`. It exposes project synchronization/status, interactive worker lifecycle, live Gherkin, Mapping operations, events/evidence, browser/service controls, semantic breakpoints, Step Override authoring, the watched-agent control lease, player-state inspection, and gated Save through `WorkbenchServices`. +The MCP adapter is `tools.dscode.workbench.mcp.WorkbenchMcpServer` plus `WorkbenchMcpTools`. It exposes project synchronization/status, interactive worker lifecycle, live Gherkin, Mapping operations, events/evidence, browser/service controls, semantic breakpoints, Step Override authoring, the watched-agent control lease, player-state inspection, gated Save, and sparse diagnostic catalog/run/summary readers through `WorkbenchServices`. Consumer agents use this headless stdio server (`mcp .`), not the Swing GUI. UI mode cannot share process stdout with stdio MCP. `ui` therefore starts a 127.0.0.1-only JSON attach facade (`WorkbenchAttachServer`) over the same tools and writes `.pickleball/workbench/attach.json` so a Copilot/MCP client can join the visible session. Bind localhost only. Do not launch a second `mcp` process against a running UI. @@ -115,9 +115,9 @@ Do not add generic IDE/file/build/process/collaboration tools to this MCP surfac ## Synchronization and worker lifecycle -Workbench synchronization is build-tool-assisted, not a replacement build system. Use the selected Maven/Gradle wrapper to establish compiled main/test output, processed resources, and the effective test runtime classpath. Gradle synchronization must use build-native init-script/task injection rather than the Gradle Tooling API. +Workbench synchronization is build-tool-assisted, not a replacement build system. Use the selected Maven/Gradle wrapper to establish compiled main/test output, processed resources, and the effective test runtime classpath. Gradle synchronization must use build-native init-script/task injection rather than the Gradle Tooling API. Compare **input** fingerprints (Java sources, resources, build files, dependency artifact bytes) to the last manifest before invoking the wrapper: skip when nothing that requires recompilation changed; run resource processing only when only feature/config/data changed; run full `test-compile` / `testClasses` when Java, the build descriptor, or dependencies changed. The output fingerprint in `manifest.json` is provenance, not the skip key. Always pass `-DskipTests`. Live Gherkin buffer edits must never require sync. If compiled project outputs are missing after a clean, escalate resources-only to full compile. -`.pickleball/workbench/base/classes` is synchronization provenance/reset state and must never be on a worker runtime classpath. `.pickleball/workbench/live/classes` is the one merged project-owned runtime root; main output is materialized first and test output overlays it so one class/resource path is visible exactly once. External dependency entries stay referenced from their normal caches. The synchronization fingerprint covers both merged project output and dependency artifact contents, so replacing a same-version local dependency still changes the snapshot identity. +`.pickleball/workbench/base/classes` is synchronization provenance/reset state and must never be on a worker runtime classpath. `.pickleball/workbench/live/classes` is the one merged project-owned runtime root; main output is materialized first and test output overlays it so one class/resource path is visible exactly once. Do not treat `live/classes` as an editor. External dependency entries stay referenced from their normal caches. The synchronization fingerprint covers both merged project output and dependency artifact contents, so replacing a same-version local dependency still changes the snapshot identity. The controller owns one interactive worker per selected project by default. Workers launch directly with Java from the existing Workbench snapshot, use the Pickleball-side worker class-name contract, and set the protocol-owned `pickleball.workbench.testOutputRoot` property so core intentionally scans the merged live root instead of relying on Maven/Gradle output suffixes. The worker PID must differ from the controller PID; its reported Pickleball code source must be exactly one captured consumer classpath entry; its version must match the synchronized manifest; and its classpath must exclude the Workbench controller artifact. Incompatible protocol/capability/origin checks fail clearly and never fall back to a bundled runtime. diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchController.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchController.java index b590ae02..a43d93c5 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchController.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchController.java @@ -16,6 +16,7 @@ import tools.dscode.workbench.player.WorkbenchSaveResult; import tools.dscode.workbench.sync.WorkbenchManifest; import tools.dscode.workbench.sync.WorkbenchSynchronizer; +import tools.dscode.workbench.diagnostics.DiagnosticEvidenceNavigator; import tools.dscode.workbench.terminal.WorkerLogFiles; import tools.dscode.workbench.worker.WorkbenchLiveSession; import tools.dscode.workbench.worker.WorkbenchWorkerStatus; @@ -35,6 +36,7 @@ public final class WorkbenchController implements WorkbenchServices { private final LiveScenarioPlayer player; private final LivePlaybackCoordinator playback; private final WorkbenchControlLease lease; + private final DiagnosticEvidenceNavigator diagnostics; private final List playerListeners = new CopyOnWriteArrayList<>(); public WorkbenchController(Path projectRoot) { @@ -44,6 +46,7 @@ public WorkbenchController(Path projectRoot) { this.player = LiveScenarioPlayer.interactiveBuffer(); this.playback = new LivePlaybackCoordinator(this.player); this.lease = new WorkbenchControlLease(); + this.diagnostics = new DiagnosticEvidenceNavigator(this.projectRoot); } @Override @@ -358,6 +361,21 @@ public int clearStepOverrides() { return mutating(live::clearStepOverrides); } + @Override + public Object diagnosticCatalog() { + return diagnostics.catalogDocument(); + } + + @Override + public Object diagnosticRun(String runId) { + return diagnostics.runDocument(runId); + } + + @Override + public Object diagnosticScenarioSummary(String runId, String scenarioId) { + return diagnostics.scenarioSummaryDocument(runId, scenarioId); + } + @Override public void close() { lease.detachUi(); diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchServices.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchServices.java index a7b5ce0f..71b8e97b 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchServices.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/WorkbenchServices.java @@ -143,6 +143,12 @@ ControlBridgeBreakpoint addBreakpoint( int clearStepOverrides(); + Object diagnosticCatalog(); + + Object diagnosticRun(String runId); + + Object diagnosticScenarioSummary(String runId, String scenarioId); + @Override void close(); } diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java index 7f582e66..daa1f6f0 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigator.java @@ -99,6 +99,66 @@ public List catalogRuns() { return List.copyOf(runs); } + public JsonNode catalogDocument() { + Path catalog = diagnosticRoot.resolve("run-catalog.json"); + var result = JSON.createObjectNode(); + result.put("available", Files.isRegularFile(catalog)); + result.put("path", catalog.toString()); + if (Files.isRegularFile(catalog)) { + result.set("catalog", readJson(catalog)); + } + return result; + } + + public JsonNode runDocument(String runId) { + Path runRoot = resolveContained(diagnosticRoot, runId, "runId"); + Path index = runRoot.resolve("run-index.json"); + Path clusters = runRoot.resolve("clusters.json"); + var result = JSON.createObjectNode(); + result.put("runId", runId); + result.put("runRoot", runRoot.toString()); + result.put("indexPresent", Files.isRegularFile(index)); + result.put("clustersPresent", Files.isRegularFile(clusters)); + if (Files.isRegularFile(index)) { + result.set("runIndex", readJson(index)); + } + if (Files.isRegularFile(clusters)) { + result.set("clusters", readJson(clusters)); + } + return result; + } + + public JsonNode scenarioSummaryDocument(String runId, String scenarioId) { + Path runRoot = resolveContained(diagnosticRoot, runId, "runId"); + Path scenarioDir = resolveContained(runRoot.resolve("scenarios"), scenarioId, "scenarioId"); + Path summary = scenarioDir.resolve("summary.json"); + if (!Files.isRegularFile(summary)) { + throw new IllegalArgumentException("No summary.json for scenario " + scenarioId + " in run " + runId); + } + var result = JSON.createObjectNode(); + result.put("runId", runId); + result.put("scenarioId", scenarioId); + result.put("path", summary.toString()); + result.set("summary", readJson(summary)); + return result; + } + + private static Path resolveContained(Path root, String name, String label) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException(label + " must not be blank."); + } + if (name.equals(".") || name.equals("..") + || name.contains("/") || name.contains("\\") || name.contains("..")) { + throw new IllegalArgumentException(label + " must be a simple directory name."); + } + Path base = root.toAbsolutePath().normalize(); + Path resolved = base.resolve(name).normalize(); + if (!resolved.startsWith(base)) { + throw new IllegalArgumentException(label + " is outside the diagnostic store."); + } + return resolved; + } + public Timeline timeline(Path runRoot) { Path root = runRoot.toAbsolutePath().normalize(); JsonNode index = readJsonIfPresent(root.resolve("run-index.json")); diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java index f8515aee..22bf3bd0 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/mcp/WorkbenchMcpTools.java @@ -193,6 +193,22 @@ private void register() { args -> Map.of("removed", services.removeStepOverride(text(args, "id")))); add("workbench_step_override_clear", "Clear all Step Overrides from the active scenario.", schema(Map.of()), args -> Map.of("removed", services.clearStepOverrides())); + + add("workbench_diagnostic_catalog", + "Read reports/diagnostic-runs/run-catalog.json as sparse JSON. Do not glob the diagnostic tree.", + schema(Map.of()), + args -> services.diagnosticCatalog()); + add("workbench_diagnostic_run", + "Read one run's run-index.json and clusters.json. Does not return events, traces, or screenshots.", + schema(Map.of("runId", stringProperty("Diagnostic run directory name from the catalog.")), "runId"), + args -> services.diagnosticRun(text(args, "runId"))); + add("workbench_diagnostic_summary", + "Read one scenario summary.json. Does not return events.jsonl, traces, or PNG bytes.", + schema(Map.of( + "runId", stringProperty("Diagnostic run directory name from the catalog."), + "scenarioId", stringProperty("Scenario directory name under that run.") + ), "runId", "scenarioId"), + args -> services.diagnosticScenarioSummary(text(args, "runId"), text(args, "scenarioId"))); } private void add( diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java index 13fe09d1..d88bdb9c 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java @@ -1,12 +1,15 @@ package tools.dscode.workbench.sync; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; /** Persisted synchronization provenance for one selected consumer project/module. */ +@JsonIgnoreProperties(ignoreUnknown = true) public record WorkbenchManifest( int schemaVersion, String projectRoot, @@ -21,16 +24,69 @@ public record WorkbenchManifest( List dependencyClasspath, String pickleballVersion, String javaVersion, - String javaHome + String javaHome, + String syncMode, + String javaInputFingerprint, + String resourceInputFingerprint, + String buildInputFingerprint, + String dependencyInputFingerprint ) { public static final int CURRENT_SCHEMA = 1; private static final ObjectMapper JSON = new ObjectMapper(); public WorkbenchManifest { - sourceRoots = List.copyOf(sourceRoots); - outputRoots = List.copyOf(outputRoots); - outputMappings = List.copyOf(outputMappings); - dependencyClasspath = List.copyOf(dependencyClasspath); + sourceRoots = List.copyOf(sourceRoots == null ? List.of() : sourceRoots); + outputRoots = List.copyOf(outputRoots == null ? List.of() : outputRoots); + outputMappings = List.copyOf(outputMappings == null ? List.of() : outputMappings); + dependencyClasspath = List.copyOf(dependencyClasspath == null ? List.of() : dependencyClasspath); + syncMode = syncMode == null || syncMode.isBlank() ? WorkbenchSyncMode.FULL.name() : syncMode; + javaInputFingerprint = javaInputFingerprint == null ? "" : javaInputFingerprint; + resourceInputFingerprint = resourceInputFingerprint == null ? "" : resourceInputFingerprint; + buildInputFingerprint = buildInputFingerprint == null ? "" : buildInputFingerprint; + dependencyInputFingerprint = dependencyInputFingerprint == null ? "" : dependencyInputFingerprint; + } + + public boolean hasInputFingerprints() { + return !javaInputFingerprint.isBlank() + && !resourceInputFingerprint.isBlank() + && !buildInputFingerprint.isBlank() + && !dependencyInputFingerprint.isBlank(); + } + + public WorkbenchManifest withSkip(String synchronizedAt, WorkbenchSyncInputs inputs) { + return new WorkbenchManifest( + schemaVersion, + projectRoot, + projectType, + buildTool, + sourceRoots, + outputRoots, + outputMappings, + liveOutput, + synchronizedAt, + fingerprint, + dependencyClasspath, + pickleballVersion, + javaVersion, + javaHome, + WorkbenchSyncMode.SKIPPED.name(), + inputs.javaFingerprint(), + inputs.resourceFingerprint(), + inputs.buildFingerprint(), + inputs.dependencyFingerprint() + ); + } + + static WorkbenchManifest readIfPresent(Path stateRoot) { + Path file = stateRoot.resolve("manifest.json"); + if (!Files.isRegularFile(file)) return null; + try { + WorkbenchManifest manifest = JSON.readValue(file.toFile(), WorkbenchManifest.class); + if (manifest.schemaVersion() != CURRENT_SCHEMA) return null; + return manifest; + } catch (IOException ignored) { + return null; + } } public static WorkbenchManifest read(Path projectRoot) { diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java new file mode 100644 index 00000000..db0b0b3b --- /dev/null +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java @@ -0,0 +1,239 @@ +package tools.dscode.workbench.sync; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.stream.Stream; + +/** + * Input-side fingerprints used to decide whether Workbench can skip the wrapper + * or refresh resources without a full test-compile. + * + *

These are distinct from {@link WorkbenchManifest#fingerprint()}, which + * remains output provenance over merged classes plus dependency artifact bytes.

+ */ +public record WorkbenchSyncInputs( + String javaFingerprint, + String resourceFingerprint, + String buildFingerprint, + String dependencyFingerprint, + List sourceRoots +) { + public WorkbenchSyncInputs { + javaFingerprint = javaFingerprint == null ? "" : javaFingerprint; + resourceFingerprint = resourceFingerprint == null ? "" : resourceFingerprint; + buildFingerprint = buildFingerprint == null ? "" : buildFingerprint; + dependencyFingerprint = dependencyFingerprint == null ? "" : dependencyFingerprint; + sourceRoots = List.copyOf(sourceRoots == null ? List.of() : sourceRoots); + } + + WorkbenchSyncInputs withDependencyFingerprint(String value) { + return new WorkbenchSyncInputs( + javaFingerprint, resourceFingerprint, buildFingerprint, value, sourceRoots + ); + } + + static WorkbenchSyncInputs capture(WorkbenchProject project, WorkbenchManifest previous) { + return capture( + project, + resolveSourceRoots(project, previous), + previous == null ? List.of() : previous.dependencyClasspath() + ); + } + + static WorkbenchSyncInputs capture( + WorkbenchProject project, + List sourceRoots, + List dependencies + ) { + List roots = sourceRoots == null || sourceRoots.isEmpty() + ? resolveSourceRoots(project, null) + : sourceRoots.stream().map(path -> path.toAbsolutePath().normalize()).toList(); + List javaFiles = new ArrayList<>(); + List resourceFiles = new ArrayList<>(); + for (Path root : roots) { + classify(root, javaFiles, resourceFiles); + } + return new WorkbenchSyncInputs( + fingerprintFiles(project.root(), javaFiles), + fingerprintFiles(project.root(), resourceFiles), + fingerprintFiles(project.root(), buildFiles(project)), + fingerprintDependencies(dependencies), + roots + ); + } + + static String fingerprintDependencies(List dependencies) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + List ordered = dependencies == null + ? List.of() + : dependencies.stream().sorted().toList(); + for (String dependency : ordered) { + if (dependency == null || dependency.isBlank()) continue; + Path path = Path.of(dependency).toAbsolutePath().normalize(); + digest.update(path.toString().getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); + if (Files.isRegularFile(path)) { + updateFileDigest(digest, path); + } else if (Files.isDirectory(path)) { + updateDirectoryDigest(digest, path); + } else { + digest.update("MISSING".getBytes(StandardCharsets.UTF_8)); + } + digest.update((byte) 0); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (Exception failure) { + throw new IllegalStateException("Could not fingerprint Workbench dependency inputs.", failure); + } + } + + static List resolveSourceRoots(WorkbenchProject project, WorkbenchManifest previous) { + if (previous != null && previous.sourceRoots() != null && !previous.sourceRoots().isEmpty()) { + return previous.sourceRoots().stream() + .map(value -> Path.of(value).toAbsolutePath().normalize()) + .toList(); + } + Path root = project.root(); + return List.of( + root.resolve("src/main/java"), + root.resolve("src/test/java"), + root.resolve("src/main/resources"), + root.resolve("src/test/resources") + ); + } + + static List buildFiles(WorkbenchProject project) { + List files = new ArrayList<>(); + Path root = project.root(); + Path buildRoot = project.buildRoot(); + if (project.type() == WorkbenchProject.Type.MAVEN) { + addIfFile(files, root.resolve("pom.xml")); + addIfFile(files, root.resolve(".mvn/maven.config")); + addIfFile(files, root.resolve(".mvn/jvm.config")); + } else { + addIfFile(files, root.resolve("build.gradle")); + addIfFile(files, root.resolve("build.gradle.kts")); + addIfFile(files, root.resolve("gradle.properties")); + addIfFile(files, buildRoot.resolve("settings.gradle")); + addIfFile(files, buildRoot.resolve("settings.gradle.kts")); + addIfFile(files, buildRoot.resolve("gradle.properties")); + addIfFile(files, buildRoot.resolve("gradle/libs.versions.toml")); + addIfFile(files, buildRoot.resolve("gradle/wrapper/gradle-wrapper.properties")); + } + return List.copyOf(files); + } + + static boolean compiledOutputsPresent(WorkbenchManifest previous) { + if (previous == null || previous.outputRoots() == null || previous.outputRoots().isEmpty()) { + return false; + } + long previousLiveClasses = countClassFiles(previous.liveOutputPath()); + long currentOutputClasses = 0; + for (WorkbenchManifest.OutputRoot output : previous.outputRoots()) { + Path path = Path.of(output.path()).toAbsolutePath().normalize(); + if (!Files.isDirectory(path)) { + return previousLiveClasses == 0; + } + currentOutputClasses += countClassFiles(path); + } + if (previousLiveClasses == 0) return true; + return currentOutputClasses >= previousLiveClasses; + } + + static long countClassFiles(Path root) { + if (root == null || !Files.isDirectory(root)) return 0; + try (Stream paths = Files.walk(root)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith(".class")) + .count(); + } catch (IOException ignored) { + return 0; + } + } + + static boolean snapshotReady(Path stateRoot) { + return Files.isDirectory(stateRoot.resolve("live").resolve("classes")) + && Files.isRegularFile(stateRoot.resolve("classpath.txt")) + && Files.isRegularFile(stateRoot.resolve("manifest.json")); + } + + private static void classify(Path root, List javaFiles, List resourceFiles) { + if (!Files.isDirectory(root)) return; + try (Stream paths = Files.walk(root)) { + paths.filter(Files::isRegularFile).forEach(file -> { + if (file.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(".java")) { + javaFiles.add(file); + } else { + resourceFiles.add(file); + } + }); + } catch (IOException failure) { + throw new IllegalStateException("Could not scan Workbench source root: " + root, failure); + } + } + + private static void addIfFile(List files, Path path) { + if (Files.isRegularFile(path)) files.add(path.toAbsolutePath().normalize()); + } + + private static String fingerprintFiles(Path projectRoot, List files) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + Path root = projectRoot.toAbsolutePath().normalize(); + List ordered = files.stream() + .map(path -> path.toAbsolutePath().normalize()) + .sorted(Comparator.comparing(path -> relativeKey(root, path))) + .toList(); + for (Path file : ordered) { + digest.update(relativeKey(root, file).getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); + if (Files.isRegularFile(file)) { + updateFileDigest(digest, file); + } else { + digest.update("MISSING".getBytes(StandardCharsets.UTF_8)); + } + digest.update((byte) 0); + } + return HexFormat.of().formatHex(digest.digest()); + } catch (Exception failure) { + throw new IllegalStateException("Could not fingerprint Workbench source inputs.", failure); + } + } + + private static String relativeKey(Path root, Path file) { + if (file.startsWith(root)) { + return root.relativize(file).toString().replace('\\', '/'); + } + return file.toString().replace('\\', '/'); + } + + private static void updateDirectoryDigest(MessageDigest digest, Path root) throws IOException { + try (var paths = Files.walk(root)) { + for (Path file : paths.filter(Files::isRegularFile).sorted().toList()) { + digest.update(root.relativize(file).toString().replace('\\', '/').getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); + updateFileDigest(digest, file); + digest.update((byte) 0); + } + } + } + + private static void updateFileDigest(MessageDigest digest, Path file) throws IOException { + try (var input = Files.newInputStream(file)) { + byte[] buffer = new byte[64 * 1024]; + int read; + while ((read = input.read(buffer)) >= 0) { + if (read > 0) digest.update(buffer, 0, read); + } + } + } +} diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java new file mode 100644 index 00000000..d5c80c3d --- /dev/null +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java @@ -0,0 +1,11 @@ +package tools.dscode.workbench.sync; + +/** How much of the consumer build wrapper a Workbench synchronization invoked. */ +public enum WorkbenchSyncMode { + /** Maven `test-compile` / Gradle `testClasses` plus classpath metadata. */ + FULL, + /** Resource processing only; Java sources and dependencies were unchanged. */ + RESOURCES_ONLY, + /** Wrapper skipped; input fingerprints matched the last recorded snapshot. */ + SKIPPED +} diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java new file mode 100644 index 00000000..d10b81af --- /dev/null +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java @@ -0,0 +1,34 @@ +package tools.dscode.workbench.sync; + +/** + * Chooses skip / resources-only / full compile from input fingerprints. + * + *

{@link WorkbenchManifest#fingerprint()} is output provenance and is never + * used as the skip key.

+ */ +public final class WorkbenchSyncPlanner { + private WorkbenchSyncPlanner() { + } + + public static WorkbenchSyncMode decide( + WorkbenchManifest previous, + WorkbenchSyncInputs current, + boolean snapshotReady + ) { + if (previous == null || current == null || !snapshotReady) { + return WorkbenchSyncMode.FULL; + } + if (!previous.hasInputFingerprints()) { + return WorkbenchSyncMode.FULL; + } + if (!current.javaFingerprint().equals(previous.javaInputFingerprint()) + || !current.buildFingerprint().equals(previous.buildInputFingerprint()) + || !current.dependencyFingerprint().equals(previous.dependencyInputFingerprint())) { + return WorkbenchSyncMode.FULL; + } + if (!current.resourceFingerprint().equals(previous.resourceInputFingerprint())) { + return WorkbenchSyncMode.RESOURCES_ONLY; + } + return WorkbenchSyncMode.SKIPPED; + } +} diff --git a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java index 78d89dae..7b100a14 100644 --- a/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java +++ b/pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java @@ -33,19 +33,47 @@ public final class WorkbenchSynchronizer { private static final ObjectMapper JSON = new ObjectMapper(); private static final String GRADLE_METADATA_PREFIX = "PKB_WORKBENCH_METADATA="; + @FunctionalInterface + interface CommandRunner { + String run(WorkbenchProject project, List args, Path log); + } + + private final CommandRunner commandRunner; + + public WorkbenchSynchronizer() { + this(null); + } + + WorkbenchSynchronizer(CommandRunner commandRunner) { + this.commandRunner = commandRunner == null ? this::runProcess : commandRunner; + } + public WorkbenchManifest sync(Path requestedProject) { WorkbenchProject project = WorkbenchProject.locate(requestedProject); Path stateRoot = WorkbenchManifest.workbenchRoot(project.root()); + WorkbenchManifest previous = WorkbenchManifest.readIfPresent(stateRoot); + WorkbenchSyncInputs inputs = WorkbenchSyncInputs.capture(project, previous); + WorkbenchSyncMode mode = WorkbenchSyncPlanner.decide( + previous, inputs, WorkbenchSyncInputs.snapshotReady(stateRoot) + ); + if (mode == WorkbenchSyncMode.RESOURCES_ONLY + && !WorkbenchSyncInputs.compiledOutputsPresent(previous)) { + mode = WorkbenchSyncMode.FULL; + } + if (mode == WorkbenchSyncMode.SKIPPED) { + return skip(previous, inputs, stateRoot); + } + Path logs = stateRoot.resolve("logs"); Path staging = stateRoot.resolve(".sync-" + UUID.randomUUID()); createDirectories(logs, staging); Path log = logs.resolve("sync-" + System.currentTimeMillis() + ".log"); try { - SyncMetadata metadata = project.type() == WorkbenchProject.Type.MAVEN - ? synchronizeMaven(project, staging, log) - : synchronizeGradle(project, staging, log); - return materialize(project, metadata, staging, stateRoot); + SyncMetadata metadata = mode == WorkbenchSyncMode.RESOURCES_ONLY + ? synchronizeResources(project, previous, log) + : synchronizeFull(project, staging, log); + return materialize(project, metadata, staging, stateRoot, mode); } finally { deleteTree(staging); } @@ -56,6 +84,22 @@ static WorkbenchManifest materialize( SyncMetadata metadata, Path staging, Path stateRoot + ) { + return materialize( + project, + metadata, + staging, + stateRoot, + WorkbenchSyncMode.FULL + ); + } + + static WorkbenchManifest materialize( + WorkbenchProject project, + SyncMetadata metadata, + Path staging, + Path stateRoot, + WorkbenchSyncMode mode ) { Path stagedBase = staging.resolve("base"); Path stagedBaseClasses = stagedBase.resolve("classes"); @@ -84,6 +128,9 @@ static WorkbenchManifest materialize( ); List dependencies = distinctExisting(metadata.dependencies()); + WorkbenchSyncInputs stored = WorkbenchSyncInputs.capture( + project, metadata.sourceRoots(), dependencies + ); String fingerprint = fingerprint(stagedBaseClasses, dependencies); Path finalBaseClasses = stateRoot.resolve("base").resolve("classes").toAbsolutePath().normalize(); Path finalLiveClasses = stateRoot.resolve("live").resolve("classes").toAbsolutePath().normalize(); @@ -116,7 +163,12 @@ static WorkbenchManifest materialize( dependencies, implementationVersion(), System.getProperty("java.version", "unknown"), - System.getProperty("java.home", "unknown") + System.getProperty("java.home", "unknown"), + mode == null ? WorkbenchSyncMode.FULL.name() : mode.name(), + stored.javaFingerprint(), + stored.resourceFingerprint(), + stored.buildFingerprint(), + stored.dependencyFingerprint() ); Path stagedManifest = staging.resolve("manifest.json"); @@ -156,10 +208,47 @@ public static List readWorkerClasspath(Path projectRoot) { } } - private SyncMetadata synchronizeMaven(WorkbenchProject project, Path staging, Path log) { + private WorkbenchManifest skip( + WorkbenchManifest previous, + WorkbenchSyncInputs inputs, + Path stateRoot + ) { + WorkbenchManifest updated = previous.withSkip(Instant.now().toString(), inputs); + updated.write(stateRoot.resolve("manifest.json")); + return updated; + } + + private SyncMetadata synchronizeFull(WorkbenchProject project, Path staging, Path log) { + return project.type() == WorkbenchProject.Type.MAVEN + ? synchronizeMavenFull(project, staging, log) + : synchronizeGradleFull(project, staging, log); + } + + private SyncMetadata synchronizeResources( + WorkbenchProject project, + WorkbenchManifest previous, + Path log + ) { + if (project.type() == WorkbenchProject.Type.MAVEN) { + commandRunner.run(project, mavenResourceArgs(project), log); + } else { + commandRunner.run(project, gradleResourceArgs(), log); + } + return metadataFrom(previous); + } + + private SyncMetadata synchronizeMavenFull(WorkbenchProject project, Path staging, Path log) { Path dependencyClasspath = staging.resolve("maven-classpath.txt"); Path effectivePom = staging.resolve("effective-pom.xml"); - List args = List.of( + commandRunner.run(project, mavenFullArgs(project, dependencyClasspath, effectivePom), log); + + MavenMetadata pom = parseEffectivePom(project.root(), effectivePom); + List dependencies = readClasspathValue(dependencyClasspath); + return new SyncMetadata(pom.sourceRoots(), pom.outputs(), dependencies); + } + + static List mavenFullArgs(WorkbenchProject project, Path dependencyClasspath, Path effectivePom) { + return List.of( "-f", project.root().resolve("pom.xml").toString(), "-DskipTests", "test-compile", @@ -170,14 +259,37 @@ private SyncMetadata synchronizeMaven(WorkbenchProject project, Path staging, Pa "org.apache.maven.plugins:maven-help-plugin:3.5.1:effective-pom", "-Doutput=" + effectivePom ); - run(project, args, log); + } + + static List mavenResourceArgs(WorkbenchProject project) { + return List.of( + "-f", project.root().resolve("pom.xml").toString(), + "-DskipTests", + "process-resources", + "process-test-resources" + ); + } - MavenMetadata pom = parseEffectivePom(project.root(), effectivePom); - List dependencies = readClasspathValue(dependencyClasspath); - return new SyncMetadata(pom.sourceRoots(), pom.outputs(), dependencies); + static List gradleResourceArgs() { + return List.of( + "processResources", + "processTestResources", + "--console=plain", + "-q" + ); + } + + private static SyncMetadata metadataFrom(WorkbenchManifest previous) { + List sources = previous.sourceRoots().stream() + .map(value -> Path.of(value).toAbsolutePath().normalize()) + .toList(); + List outputs = previous.outputRoots().stream() + .map(output -> new OutputPath(output.kind(), Path.of(output.path()))) + .toList(); + return new SyncMetadata(sources, outputs, previous.dependencyClasspath()); } - private SyncMetadata synchronizeGradle(WorkbenchProject project, Path staging, Path log) { + private SyncMetadata synchronizeGradleFull(WorkbenchProject project, Path staging, Path log) { Path initScript = staging.resolve("workbench-sync.init.gradle"); writeString(initScript, gradleInitScript()); List args = List.of( @@ -187,7 +299,7 @@ private SyncMetadata synchronizeGradle(WorkbenchProject project, Path staging, P "--console=plain", "-q" ); - String output = run(project, args, log); + String output = commandRunner.run(project, args, log); String metadataLine = output.lines() .filter(line -> line.startsWith(GRADLE_METADATA_PREFIX)) .reduce((first, second) -> second) @@ -234,7 +346,7 @@ static MavenMetadata parseEffectivePom(Path projectRoot, Path effectivePom) { } } - private String run(WorkbenchProject project, List args, Path log) { + private String runProcess(WorkbenchProject project, List args, Path log) { List command = executableCommand(project.launcher(), args); ProcessBuilder builder = new ProcessBuilder(command) .directory(project.buildRoot().toFile()) diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java index 72e6c52b..7c6b9944 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/catalog/ConsumerFeatureCatalogTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import tools.dscode.workbench.sync.WorkbenchManifest; +import tools.dscode.workbench.sync.WorkbenchSyncMode; import java.nio.file.Files; import java.nio.file.Path; @@ -226,7 +227,12 @@ void prefersClasspathFeaturesConfigurationWithoutCrawlingUnrelatedDirectories() List.of(), "2.1.9", "21", - "/usr/lib/jvm" + "/usr/lib/jvm", + WorkbenchSyncMode.FULL.name(), + "java-fp", + "resource-fp", + "build-fp", + "dep-fp" ); ConsumerFeatureCatalog catalog = ConsumerFeatureCatalog.scan(project, manifest); diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java index 81ae6c50..3cba86ff 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/diagnostics/DiagnosticEvidenceNavigatorTest.java @@ -7,6 +7,7 @@ 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 DiagnosticEvidenceNavigatorTest { @@ -48,4 +49,42 @@ void readsCatalogThenIndexAndScreenshotFramesWithoutInventingRuns() throws Excep .anyMatch(layer -> layer.layer() == DiagnosticEvidenceNavigator.Layer.EVENTS && layer.present())); assertTrue(new DiagnosticEvidenceNavigator(project, project.resolve("missing")).catalogRuns().isEmpty()); } + + @Test + void sparseReadersReturnCatalogIndexClustersAndSummaryWithoutEventsOrScreenshots() throws Exception { + Path root = project.resolve("reports/diagnostic-runs"); + Path run = root.resolve("run-1"); + Path scenario = run.resolve("scenarios/scenario-1"); + Files.createDirectories(scenario.resolve("screenshots")); + Files.writeString(root.resolve("run-catalog.json"), """ + {"runs":[{"runId":"run-1","outcome":"FAILED"}]} + """); + Files.writeString(run.resolve("run-index.json"), """ + {"runId":"run-1","outcome":"FAILED","scenarioCount":1} + """); + Files.writeString(run.resolve("clusters.json"), """ + {"clusters":[{"id":"c1","size":1}]} + """); + Files.writeString(scenario.resolve("summary.json"), """ + {"scenarioId":"scenario-1","outcome":"FAILED","lastStepText":"Then stay"} + """); + Files.writeString(scenario.resolve("events.jsonl"), "{\"stepText\":\"secret-event\"}\n"); + Files.write(scenario.resolve("screenshots/frame-1.png"), new byte[]{9, 9, 9}); + + DiagnosticEvidenceNavigator navigator = new DiagnosticEvidenceNavigator(project); + String catalog = navigator.catalogDocument().toString(); + assertTrue(catalog.contains("run-1")); + assertFalse(catalog.contains("secret-event")); + + String runDocument = navigator.runDocument("run-1").toString(); + assertTrue(runDocument.contains("FAILED")); + assertTrue(runDocument.contains("\"clusters\"")); + assertFalse(runDocument.contains("secret-event")); + assertFalse(runDocument.contains("frame-1.png")); + + String summary = navigator.scenarioSummaryDocument("run-1", "scenario-1").toString(); + assertTrue(summary.contains("Then stay")); + assertFalse(summary.contains("secret-event")); + assertFalse(summary.contains("frame-1.png")); + } } diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java index a1096472..a2c6e053 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/mcp/WorkbenchMcpServerTest.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; @@ -67,6 +68,9 @@ void packagedServerInitializesListsToolsInvokesControllerAndKeepsStdoutProtocolO assertTrue(toolNames.contains("workbench_request_control")); assertTrue(toolNames.contains("workbench_player_state")); assertTrue(toolNames.contains("workbench_request_save")); + assertTrue(toolNames.contains("workbench_diagnostic_catalog")); + assertTrue(toolNames.contains("workbench_diagnostic_run")); + assertTrue(toolNames.contains("workbench_diagnostic_summary")); JsonNode status = harness.toolCall(3, "workbench_worker_status", "{}"); assertFalse(status.at("/result/isError").asBoolean()); @@ -129,6 +133,43 @@ void stepOverrideCompileUsesSharedServicesAndServerCloseIsIdempotent() throws Ex assertEquals(1, closes.get()); } + @Test + void diagnosticToolsDelegateToSharedServicesAndStaySparse() throws Exception { + AtomicBoolean catalogCalled = new AtomicBoolean(); + WorkbenchServices services = fakeServices((method, args) -> { + if ("diagnosticCatalog".equals(method)) { + catalogCalled.set(true); + return Map.of("available", true, "catalog", Map.of("runs", List.of(Map.of("runId", "run-1")))); + } + if ("diagnosticRun".equals(method)) { + assertEquals("run-1", args[0]); + return Map.of("runId", "run-1", "runIndex", Map.of("outcome", "FAILED")); + } + if ("diagnosticScenarioSummary".equals(method)) { + assertEquals("run-1", args[0]); + assertEquals("scenario-1", args[1]); + return Map.of("summary", Map.of("outcome", "FAILED")); + } + if ("close".equals(method)) return null; + return null; + }); + + WorkbenchMcpTools tools = new WorkbenchMcpTools(services, JSON); + assertTrue(tools.names().contains("workbench_diagnostic_catalog")); + Object catalog = tools.call("workbench_diagnostic_catalog", Map.of()); + assertTrue(catalogCalled.get()); + assertTrue(JSON.writeValueAsString(catalog).contains("run-1")); + assertFalse(JSON.writeValueAsString(catalog).contains("events.jsonl")); + + Object run = tools.call("workbench_diagnostic_run", Map.of("runId", "run-1")); + assertTrue(JSON.writeValueAsString(run).contains("FAILED")); + Object summary = tools.call("workbench_diagnostic_summary", Map.of( + "runId", "run-1", + "scenarioId", "scenario-1" + )); + assertTrue(JSON.writeValueAsString(summary).contains("FAILED")); + } + private static WorkbenchServices fakeServices(FakeInvocation invocation) { return (WorkbenchServices) Proxy.newProxyInstance( WorkbenchServices.class.getClassLoader(), diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java new file mode 100644 index 00000000..3875e328 --- /dev/null +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java @@ -0,0 +1,90 @@ +package tools.dscode.workbench.sync; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class WorkbenchSyncPlannerTest { + + @Test + void missingPreviousOrSnapshotRequiresFullCompile() { + WorkbenchSyncInputs inputs = inputs("java", "res", "build", "dep"); + assertEquals(WorkbenchSyncMode.FULL, WorkbenchSyncPlanner.decide(null, inputs, true)); + assertEquals(WorkbenchSyncMode.FULL, WorkbenchSyncPlanner.decide(manifest(inputs), inputs, false)); + } + + @Test + void blankInputFingerprintsRequireFullCompile() { + WorkbenchManifest previous = manifest(new WorkbenchSyncInputs("", "", "", "", List.of())); + WorkbenchSyncInputs current = inputs("java", "res", "build", "dep"); + assertEquals(WorkbenchSyncMode.FULL, WorkbenchSyncPlanner.decide(previous, current, true)); + } + + @Test + void matchingInputFingerprintsSkipTheWrapper() { + WorkbenchSyncInputs inputs = inputs("java", "res", "build", "dep"); + assertEquals(WorkbenchSyncMode.SKIPPED, WorkbenchSyncPlanner.decide(manifest(inputs), inputs, true)); + } + + @Test + void resourceOnlyChangeSelectsResourcesOnly() { + WorkbenchManifest previous = manifest(inputs("java", "res", "build", "dep")); + WorkbenchSyncInputs current = inputs("java", "res-changed", "build", "dep"); + assertEquals(WorkbenchSyncMode.RESOURCES_ONLY, WorkbenchSyncPlanner.decide(previous, current, true)); + } + + @Test + void javaBuildOrDependencyChangeSelectsFullCompile() { + WorkbenchManifest previous = manifest(inputs("java", "res", "build", "dep")); + assertEquals( + WorkbenchSyncMode.FULL, + WorkbenchSyncPlanner.decide(previous, inputs("java-changed", "res", "build", "dep"), true) + ); + assertEquals( + WorkbenchSyncMode.FULL, + WorkbenchSyncPlanner.decide(previous, inputs("java", "res", "build-changed", "dep"), true) + ); + assertEquals( + WorkbenchSyncMode.FULL, + WorkbenchSyncPlanner.decide(previous, inputs("java", "res", "build", "dep-changed"), true) + ); + } + + @Test + void javaChangeWinsOverResourceChange() { + WorkbenchManifest previous = manifest(inputs("java", "res", "build", "dep")); + WorkbenchSyncInputs current = inputs("java-changed", "res-changed", "build", "dep"); + assertEquals(WorkbenchSyncMode.FULL, WorkbenchSyncPlanner.decide(previous, current, true)); + } + + private static WorkbenchSyncInputs inputs(String java, String resource, String build, String dependency) { + return new WorkbenchSyncInputs(java, resource, build, dependency, List.of(Path.of("src/test/java"))); + } + + private static WorkbenchManifest manifest(WorkbenchSyncInputs inputs) { + return new WorkbenchManifest( + WorkbenchManifest.CURRENT_SCHEMA, + "/project", + "MAVEN", + "mvn", + List.of("/project/src/test/java"), + List.of(new WorkbenchManifest.OutputRoot("TEST", "/project/target/test-classes")), + List.of(), + "/project/.pickleball/workbench/live/classes", + "2026-08-25T00:00:00Z", + "output-fp", + List.of("/repo/dep.jar"), + "2.1.9", + "21", + "/usr/lib/jvm", + WorkbenchSyncMode.FULL.name(), + inputs.javaFingerprint(), + inputs.resourceFingerprint(), + inputs.buildFingerprint(), + inputs.dependencyFingerprint() + ); + } +} diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java index e9b18ff0..726b4d16 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java @@ -6,6 +6,7 @@ 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; @@ -167,4 +168,201 @@ void detectsMavenAndGradleSelectedProjects() throws Exception { Files.writeString(gradle.resolve("build.gradle"), "plugins { id 'java' }"); assertEquals(WorkbenchProject.Type.GRADLE, WorkbenchProject.locate(gradle).type()); } + + @Test + void mavenFullArgsSkipTestsAndNeverInvokeSurefire() { + WorkbenchProject project = new WorkbenchProject( + tempDir.resolve("consumer").toAbsolutePath().normalize(), + WorkbenchProject.Type.MAVEN, + tempDir.resolve("consumer").toAbsolutePath().normalize(), + Path.of("mvn") + ); + List full = WorkbenchSynchronizer.mavenFullArgs( + project, tempDir.resolve("cp.txt"), tempDir.resolve("pom.xml") + ); + List resources = WorkbenchSynchronizer.mavenResourceArgs(project); + assertTrue(full.contains("-DskipTests")); + assertTrue(full.contains("test-compile")); + assertFalse(full.contains("test")); + assertTrue(resources.contains("-DskipTests")); + assertTrue(resources.contains("process-test-resources")); + assertFalse(resources.contains("test-compile")); + assertTrue(WorkbenchSynchronizer.gradleResourceArgs().contains("processTestResources")); + assertFalse(WorkbenchSynchronizer.gradleResourceArgs().contains("testClasses")); + } + + @Test + void skipsWrapperWhenJavaResourcesAndBuildAreUnchanged() throws Exception { + MavenFixture fixture = MavenFixture.create(tempDir.resolve("skip-consumer")); + RecordingRunner runner = fixture.runner(); + WorkbenchSynchronizer synchronizer = new WorkbenchSynchronizer(runner); + + WorkbenchManifest first = synchronizer.sync(fixture.projectRoot); + assertEquals(WorkbenchSyncMode.FULL.name(), first.syncMode()); + assertEquals(1, runner.invocations.size()); + assertTrue(runner.invocations.getFirst().contains("test-compile")); + assertTrue(first.hasInputFingerprints()); + + String liveMarker = Files.readString(Path.of(first.liveOutput()).resolve("Runner.class")); + WorkbenchManifest second = synchronizer.sync(fixture.projectRoot); + assertEquals(WorkbenchSyncMode.SKIPPED.name(), second.syncMode()); + assertEquals(1, runner.invocations.size()); + assertEquals(first.fingerprint(), second.fingerprint()); + assertEquals(liveMarker, Files.readString(Path.of(second.liveOutput()).resolve("Runner.class"))); + } + + @Test + void refreshesResourcesWithoutTestCompileWhenOnlyFeaturesChanged() throws Exception { + MavenFixture fixture = MavenFixture.create(tempDir.resolve("resource-consumer")); + RecordingRunner runner = fixture.runner(); + WorkbenchSynchronizer synchronizer = new WorkbenchSynchronizer(runner); + WorkbenchManifest first = synchronizer.sync(fixture.projectRoot); + assertEquals(WorkbenchSyncMode.FULL.name(), first.syncMode()); + + Files.writeString( + fixture.projectRoot.resolve("src/test/resources/features/demo.feature"), + "Feature: changed\n", + StandardCharsets.UTF_8 + ); + WorkbenchManifest second = synchronizer.sync(fixture.projectRoot); + assertEquals(WorkbenchSyncMode.RESOURCES_ONLY.name(), second.syncMode()); + assertEquals(2, runner.invocations.size()); + assertTrue(runner.invocations.get(1).contains("process-test-resources")); + assertFalse(runner.invocations.get(1).contains("test-compile")); + assertEquals( + "Feature: changed\n", + Files.readString(Path.of(second.liveOutput()).resolve("features/demo.feature")) + ); + assertTrue(Files.exists(Path.of(second.liveOutput()).resolve("Runner.class"))); + } + + @Test + void runsFullCompileWhenJavaSourceChanges() throws Exception { + MavenFixture fixture = MavenFixture.create(tempDir.resolve("java-consumer")); + RecordingRunner runner = fixture.runner(); + WorkbenchSynchronizer synchronizer = new WorkbenchSynchronizer(runner); + synchronizer.sync(fixture.projectRoot); + + Files.writeString( + fixture.projectRoot.resolve("src/test/java/Runner.java"), + "class Runner { int n = 2; }\n", + StandardCharsets.UTF_8 + ); + WorkbenchManifest second = synchronizer.sync(fixture.projectRoot); + assertEquals(WorkbenchSyncMode.FULL.name(), second.syncMode()); + assertEquals(2, runner.invocations.size()); + assertTrue(runner.invocations.get(1).contains("test-compile")); + } + + private static final class RecordingRunner implements WorkbenchSynchronizer.CommandRunner { + private final List> invocations = new ArrayList<>(); + private final MavenFixture fixture; + + private RecordingRunner(MavenFixture fixture) { + this.fixture = fixture; + } + + @Override + public String run(WorkbenchProject project, List args, Path log) { + invocations.add(List.copyOf(args)); + try { + if (args.contains("test-compile")) { + fixture.writeCompileOutputs(args); + } else if (args.contains("process-test-resources")) { + fixture.writeResourceOutputs(); + } + } catch (Exception failure) { + throw new IllegalStateException("Fake Maven runner failed.", failure); + } + return ""; + } + } + + private static final class MavenFixture { + private final Path projectRoot; + private final Path dependency; + private final RecordingRunner runner = new RecordingRunner(this); + + private MavenFixture(Path projectRoot, Path dependency) { + this.projectRoot = projectRoot; + this.dependency = dependency; + } + + static MavenFixture create(Path projectRoot) throws Exception { + Files.createDirectories(projectRoot.resolve("src/test/java")); + Files.createDirectories(projectRoot.resolve("src/test/resources/features")); + Files.writeString(projectRoot.resolve("pom.xml"), "", StandardCharsets.UTF_8); + Files.writeString( + projectRoot.resolve("src/test/java/Runner.java"), + "class Runner {}\n", + StandardCharsets.UTF_8 + ); + Files.writeString( + projectRoot.resolve("src/test/resources/features/demo.feature"), + "Feature: demo\n", + StandardCharsets.UTF_8 + ); + Path dependency = projectRoot.resolve("dep.jar"); + Files.write(dependency, new byte[]{1, 2, 3}); + return new MavenFixture(projectRoot.toAbsolutePath().normalize(), dependency); + } + + RecordingRunner runner() { + return runner; + } + + void writeCompileOutputs(List args) throws Exception { + Path main = projectRoot.resolve("target/classes"); + Path test = projectRoot.resolve("target/test-classes"); + Files.createDirectories(main); + Files.createDirectories(test.resolve("features")); + Files.writeString(test.resolve("Runner.class"), "class-bytes", StandardCharsets.UTF_8); + Files.writeString( + test.resolve("features/demo.feature"), + Files.readString(projectRoot.resolve("src/test/resources/features/demo.feature")), + StandardCharsets.UTF_8 + ); + Path classpath = outputFile(args, "-Dmdep.outputFile="); + Path effectivePom = outputFile(args, "-Doutput="); + Files.writeString(classpath, dependency.toString(), StandardCharsets.UTF_8); + Files.writeString(effectivePom, """ + + + + %s + %s + %s + %s + %s + %s + + + """.formatted( + projectRoot.resolve("src/main/java"), + projectRoot.resolve("src/test/java"), + projectRoot.resolve("src/main/resources"), + projectRoot.resolve("src/test/resources"), + main.toAbsolutePath(), + test.toAbsolutePath() + ), StandardCharsets.UTF_8); + } + + void writeResourceOutputs() throws Exception { + Path test = projectRoot.resolve("target/test-classes"); + Files.createDirectories(test.resolve("features")); + Files.writeString( + test.resolve("features/demo.feature"), + Files.readString(projectRoot.resolve("src/test/resources/features/demo.feature")), + StandardCharsets.UTF_8 + ); + } + + private static Path outputFile(List args, String prefix) { + return args.stream() + .filter(arg -> arg.startsWith(prefix)) + .map(arg -> Path.of(arg.substring(prefix.length()))) + .findFirst() + .orElseThrow(); + } + } } diff --git a/pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java b/pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java index f8b6a237..57d58bb8 100644 --- a/pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java +++ b/pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java @@ -26,6 +26,7 @@ import tools.dscode.workbench.lease.WorkbenchControlLease; import tools.dscode.workbench.lease.WorkbenchControlLeaseSnapshot; import tools.dscode.workbench.sync.WorkbenchManifest; +import tools.dscode.workbench.sync.WorkbenchSyncMode; import tools.dscode.workbench.worker.WorkbenchWorkerStatus; import java.lang.reflect.Proxy; @@ -310,7 +311,12 @@ private static WorkbenchManifest manifest() { List.of(), "2.1.8", "21", - Path.of("java-home").toAbsolutePath().normalize().toString() + Path.of("java-home").toAbsolutePath().normalize().toString(), + WorkbenchSyncMode.FULL.name(), + "java-fp", + "resource-fp", + "build-fp", + "dep-fp" ); } 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 a4456e10..90ccf940 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 @@ -5,6 +5,7 @@ import tools.dscode.control.protocol.ControlBridgeDescriptor; import tools.dscode.control.protocol.ControlProtocol; import tools.dscode.workbench.sync.WorkbenchManifest; +import tools.dscode.workbench.sync.WorkbenchSyncMode; import java.io.File; import java.nio.file.Path; @@ -211,7 +212,12 @@ private WorkbenchManifest manifest(Path project, Path live, Path dependency) { List.of(dependency.toString()), "2.1.8", "21", - System.getProperty("java.home") + System.getProperty("java.home"), + WorkbenchSyncMode.FULL.name(), + "java-fp", + "resource-fp", + "build-fp", + "dep-fp" ); } 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 9160a940..ac261637 100644 --- a/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md +++ b/src/main/resources/META-INF/pickleball/guidance/AGENT-GUIDE.md @@ -2,7 +2,53 @@ 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/`. +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. + +## Tool chooser + +Use this order. Consumer AI agents for this Pickleball release use headless Workbench MCP (`mcp .`). Do not start the Workbench GUI. + +1. **Live headless MCP** — isolate a failing step in a paused worker. Reuse compilation, rewrite Gherkin in the live buffer, and inspect the page and semantic events in the same browser/Mapping state. +2. **One diagnostic `mvn test`** — after the live loop has isolated the failure, run one bounded confirmation with `pkb_runvars` so an evidence pack is retained. +3. **Edit real consumer source** — change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. + +Do not copy consumer features into `.pickleball` as a sandbox. + +### Live isolation loop + +From the consumer project, with Pickleball on the test classpath: + +1. Start the launcher with `mcp .` (not a GUI command). +2. Call `workbench_sync` once. The synchronizer skips Maven/Gradle when Java/build/dependencies are unchanged, and refreshes test resources without a full `test-compile` when only features/config/data changed. +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 `workbench_events`. +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`. + +`workbench_execute_step` failure does not end the worker. Insert, nest, or retry in the same paused browser/Mapping state. 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. + +### Generated trees are not the project + +- `.pickleball/maven-consumer-project/` is a version-matched **read-only** reference snapshot of Pickleball's own example consumer. Do not copy, edit, or execute it as the project under test. +- `.pickleball/workbench/live/classes` is the compiled overlay for the worker classpath. Do not use it as an editor. +- `export-guidance` does **not** copy this consumer's own features into `.pickleball` for testing. It still materializes full `docs/` plus the example-consumer snapshot for on-demand/human use. + +## First-read + +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 and live loop. +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. + +Do not read `docs/README.md`, the whole `maven-consumer-project/` snapshot, or Workbench GUI pages as first actions. Those remain available on demand. + +The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. ## Generated guidance lifecycle @@ -19,26 +65,13 @@ The exporter does not create/commit a new `.gitignore`, alter the Git index, or Compatibility note: an older Pickleball release whose exporter predates the manifest lifecycle may leave newer files behind after a downgrade. Those leftovers are not authoritative for the downgraded dependency. Prefer the dependency actually resolved on the test classpath and files freshly exported by that dependency. -## First actions - -For Pickleball scenario authoring, configuration, execution, diagnostics, or troubleshooting: - -1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Read this guide before changing Pickleball scenarios or diagnosing a Pickleball run. -3. Use `docs/README.md` as the documentation map. -4. Inspect the consumer project's `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. -5. Use `maven-consumer-project/` as a version-matched read-only reference when a documented syntax/configuration example or working Pickleball consumer structure is useful. -6. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. - -The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. - ## Generated Maven consumer reference -`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. +`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. It is not the consumer project under test and is not a writable sandbox. The snapshot intentionally includes the consumer `pom.xml`, Pickleball runner, local browser/service test server, executable feature files, service-call definitions, configuration/data fixtures, local test-site resources, and the committed shared/local profile and property examples. It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` bridge, internal Java verification classes, and maintainer-only `_local2` files. -Use the snapshot to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not modify or execute files under `.pickleball/maven-consumer-project/` as the consumer project's implementation. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. +Use the snapshot only to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not copy, modify, or execute files under `.pickleball/maven-consumer-project/` as the project under test. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. `export-guidance` does not copy the consumer's own features into `.pickleball` for testing. ## Scenario authoring and fixes @@ -100,6 +133,22 @@ 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 and keep the selection narrow. Documented AI defaults: + +```text +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. + +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`). Example confirmation after a live-loop isolation: + +```text +mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: ```text @@ -139,6 +188,8 @@ Use this escalation order: Stop reading as soon as the current layer answers the investigation. Do not recursively ingest an entire diagnostic run. +From headless Workbench MCP, use `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` for layers 1–3 instead of globbing `reports/diagnostic-runs`. Those tools return sparse JSON only and do not dump `events.jsonl`, traces, or screenshot bytes. + ## Visual evidence rules - Never open a PNG merely to determine whether two screenshots differ. @@ -198,7 +249,7 @@ See `docs/ai-run-configuration.md` for the full profile/RunVar contract and `doc ## Pickleball syntax documentation -The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Use `docs/README.md` to select the relevant guide. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: +The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Open a specific guide when the live loop or a diagnostic layer requires that topic; do not start by reading `docs/README.md` as a dump. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: - dynamic Gherkin/action/assertion syntax — `docs/dynamic-steps.md`; - element vocabulary/selectors — `docs/custom-element-definitions.md`; @@ -217,7 +268,7 @@ Do not guess Pickleball syntax when the version-matched guide or executable cons ## Human-readable consumer guidance -Use `docs/consumer-project.md` for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Use `docs/README.md` to navigate the complete bundled documentation. Human readers can open files under `maven-consumer-project/` directly in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples linked from those guides. +Use `docs/consumer-project.md` on demand for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Human readers can start with `docs/README.md` and open files under `maven-consumer-project/` in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples. Agents should not treat those as first-read. ## When the core Pickleball repository is also present 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 6f1ae4cd..3d8d3cb7 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 and AI agents can therefore inspect the same working features, configuration, calls, data, runner, and local test-site examples without checking out the Pickleball source repository. +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. ## Start here 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 8fcff1d1..e4121374 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 @@ -6,7 +6,7 @@ This file maps consumer-visible capabilities to implementation anchors, executab |---|---|---|---| | Build, publication, Java compatibility, nested controller distribution | `build.gradle`; `settings.gradle`; `pickleball-control-protocol/build.gradle`; `pickleball-workbench/build.gradle`; `src/main/aspectj`; search `verifyStrictControllerIsolation`, `verifyEmbeddedWorkbench`, `shadowJar`, `JavaLanguageVersion` | root tests; protocol `check`; `:pickleball-workbench:test`; artifact/dependency verification; Maven consumer build | `README.md`; `docs/getting-started.md`; `docs/cucumber-compatibility.md`; `docs/consumer-project.md`; `docs/pickleball-workbench.md` | | Neutral controller/worker wire protocol | `pickleball-control-protocol/src/main/java/tools/dscode/control/protocol`; `ControlProtocol`; `ControlBridgeRequests`; `ControlBridgeResponses`; immutable `ControlBridge*` records | `verifyProtocolIsolation`; `ControlBridgeClientTest`; consumer `@control-bridge`; protocol/version/capability assertions | `docs/pickleball-workbench.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`; `WorkbenchWorkerManager`; `WorkbenchLiveSession`; `tools.dscode.workbench.bridge.ControlBridgeClient`; `tools.dscode.workbench.mcp`; `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`; `WorkbenchUiControllerTest`; `LiveScenarioPlayerTest`; packaged UI/MCP probes | `docs/pickleball-workbench.md`; `docs/pickleball-workbench-player.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`; `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`; `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` | | 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` | @@ -41,12 +41,12 @@ The standalone Workbench shadow JAR is controller-only and self-contained, with The canonical worker bootstrap environment is `PKB_CONTROL_BRIDGE_SESSION_DIR`, `PKB_CONTROL_BRIDGE_SESSION_ID`, `PKB_CONTROL_BRIDGE_TOKEN`, and `PKB_CONTROL_BRIDGE_PAUSE_FIRST_SCENARIO`. Pickleball may also accept the prior `PKB_STUDIO_BRIDGE_*` names as deprecated compatibility input aliases. New Workbench code must use only the neutral names. -`WorkbenchSynchronizer` uses the selected Maven/Gradle wrapper to run the minimum test-compilation/resource lifecycle and capture the effective test runtime dependency classpath; Gradle metadata is obtained with a temporary init script rather than the Gradle Tooling API. It materializes `.pickleball/workbench/base/classes` as immutable synchronization provenance and one merged `.pickleball/workbench/live/classes` runtime root, applying main output first and test output second so test-owned paths win deterministically. `base` is never placed on `classpath.txt` or a worker classpath. The synchronization fingerprint includes dependency artifact contents as well as merged project output. +`WorkbenchSynchronizer` uses the selected Maven/Gradle wrapper to run the minimum test-compilation/resource lifecycle and capture the effective test runtime dependency classpath; Gradle metadata is obtained with a temporary init script rather than the Gradle Tooling API. Input fingerprints of Java sources, resources, build files, and dependency artifacts decide skip vs resources-only vs full compile; the output fingerprint in `manifest.json` remains provenance, not a skip key. Sync always passes `-DskipTests`. It materializes `.pickleball/workbench/base/classes` as immutable synchronization provenance and one merged `.pickleball/workbench/live/classes` runtime root, applying main output first and test output second so test-owned paths win deterministically. `base` is never placed on `classpath.txt` or a worker classpath. The synchronization fingerprint includes dependency artifact contents as well as merged project output. Live Gherkin buffer edits do not require sync. `WorkbenchWorkerManager` launches a consumer JVM directly from that live root plus captured dependencies through the protocol-owned worker class-name contract, without linking the worker class or invoking Maven/Gradle. Interactive workers use a session-private anchor feature and a one-shot `BEFORE_STEP` breakpoint to reach an initialized, paused marker before the controller returns a live worker. Pause leases remain finite and are renewed while the controller owns the anchor. `WorkbenchLiveSession` binds operations to the controller-owned paused scenario and verifies worker PID, bridge runtime id, and scenario id stability. 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). UI mode writes a localhost attach endpoint to `.pickleball/workbench/attach.json` so an agent joins the same JVM/worker instead of starting a second Workbench. 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 headless MCP (`mcp .`). 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 163dbf16..f44d9c18 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 @@ -505,6 +505,9 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchManifest.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchProject.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSynchronizer.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncInputs.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncMode.java` +- `pickleball-workbench/src/main/java/tools/dscode/workbench/sync/WorkbenchSyncPlanner.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/terminal/WorkerLogBuffer.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/terminal/WorkerLogFiles.java` - `pickleball-workbench/src/main/java/tools/dscode/workbench/ui/FeaturePickerPanel.java` @@ -550,6 +553,7 @@ This inventory helps coding agents discover relevant files. It does not replace - `pickleball-workbench/src/test/java/tools/dscode/workbench/player/LiveScenarioPlayerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchGradleSynchronizerIntegrationTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSynchronizerTest.java` +- `pickleball-workbench/src/test/java/tools/dscode/workbench/sync/WorkbenchSyncPlannerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/terminal/WorkerLogBufferTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/ui/WorkbenchUiControllerTest.java` - `pickleball-workbench/src/test/java/tools/dscode/workbench/WorkbenchApplicationTest.java` 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 97ef5401..109a9fda 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 @@ -243,6 +243,12 @@ Example compact rerun: -Dpkb_changed_variables=pkb_browser ``` +For an agent's bounded confirmation `mvn test` (not `PickleballTests` human defaults of `pretty` / `@all`), include diagnostic evidence controls and keep selection narrow: + +```text +-Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + Lineage metadata is not execution configuration: ```text 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 9160a940..ac261637 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,7 +2,53 @@ 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/`. +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. + +## Tool chooser + +Use this order. Consumer AI agents for this Pickleball release use headless Workbench MCP (`mcp .`). Do not start the Workbench GUI. + +1. **Live headless MCP** — isolate a failing step in a paused worker. Reuse compilation, rewrite Gherkin in the live buffer, and inspect the page and semantic events in the same browser/Mapping state. +2. **One diagnostic `mvn test`** — after the live loop has isolated the failure, run one bounded confirmation with `pkb_runvars` so an evidence pack is retained. +3. **Edit real consumer source** — change the project's own features/Java only after the live buffer is right. Explicit Save is what writes a `.feature` file. + +Do not copy consumer features into `.pickleball` as a sandbox. + +### Live isolation loop + +From the consumer project, with Pickleball on the test classpath: + +1. Start the launcher with `mcp .` (not a GUI command). +2. Call `workbench_sync` once. The synchronizer skips Maven/Gradle when Java/build/dependencies are unchanged, and refreshes test resources without a full `test-compile` when only features/config/data changed. +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 `workbench_events`. +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`. + +`workbench_execute_step` failure does not end the worker. Insert, nest, or retry in the same paused browser/Mapping state. 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. + +### Generated trees are not the project + +- `.pickleball/maven-consumer-project/` is a version-matched **read-only** reference snapshot of Pickleball's own example consumer. Do not copy, edit, or execute it as the project under test. +- `.pickleball/workbench/live/classes` is the compiled overlay for the worker classpath. Do not use it as an editor. +- `export-guidance` does **not** copy this consumer's own features into `.pickleball` for testing. It still materializes full `docs/` plus the example-consumer snapshot for on-demand/human use. + +## First-read + +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 and live loop. +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. + +Do not read `docs/README.md`, the whole `maven-consumer-project/` snapshot, or Workbench GUI pages as first actions. Those remain available on demand. + +The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. ## Generated guidance lifecycle @@ -19,26 +65,13 @@ The exporter does not create/commit a new `.gitignore`, alter the Git index, or Compatibility note: an older Pickleball release whose exporter predates the manifest lifecycle may leave newer files behind after a downgrade. Those leftovers are not authoritative for the downgraded dependency. Prefer the dependency actually resolved on the test classpath and files freshly exported by that dependency. -## First actions - -For Pickleball scenario authoring, configuration, execution, diagnostics, or troubleshooting: - -1. Follow the consumer project's own instructions first; they remain authoritative for project-specific behavior. -2. Read this guide before changing Pickleball scenarios or diagnosing a Pickleball run. -3. Use `docs/README.md` as the documentation map. -4. Inspect the consumer project's `pom.xml`, Pickleball runner subclass, features, configuration, data, mappings, and test support before changing them. -5. Use `maven-consumer-project/` as a version-matched read-only reference when a documented syntax/configuration example or working Pickleball consumer structure is useful. -6. Do not assume the Pickleball core source repository is present. A normal consumer may only have the Maven dependency. - -The exported documentation and Maven consumer reference are version-matched to the Pickleball artifact on the consumer's test classpath. Prefer them over instructions or examples copied from another release. - ## Generated Maven consumer reference -`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. +`.pickleball/maven-consumer-project/` is a generated, read-only reference snapshot of the canonical Maven consumer used by Pickleball itself. It preserves repository-relative paths so links from the exported Markdown documentation continue to resolve locally. It is not the consumer project under test and is not a writable sandbox. The snapshot intentionally includes the consumer `pom.xml`, Pickleball runner, local browser/service test server, executable feature files, service-call definitions, configuration/data fixtures, local test-site resources, and the committed shared/local profile and property examples. It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` bridge, internal Java verification classes, and maintainer-only `_local2` files. -Use the snapshot to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not modify or execute files under `.pickleball/maven-consumer-project/` as the consumer project's implementation. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. +Use the snapshot only to answer questions such as how a working feature, profile, property file, service call, configuration resource, browser fixture, or runner is structured. Do not copy, modify, or execute files under `.pickleball/maven-consumer-project/` as the project under test. Make requested changes in the consumer project's own source tree. A later `export-guidance` run may overwrite or remove every managed reference file. `export-guidance` does not copy the consumer's own features into `.pickleball` for testing. ## Scenario authoring and fixes @@ -100,6 +133,22 @@ 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 and keep the selection narrow. Documented AI defaults: + +```text +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. + +These are documented agent defaults, not `PickleballTests` human defaults (`pretty`, `@all`). Example confirmation after a live-loop isolation: + +```text +mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + A selected profile or partial `pkb_runvars` input inherits only missing project execution-context RunVars: ```text @@ -139,6 +188,8 @@ Use this escalation order: Stop reading as soon as the current layer answers the investigation. Do not recursively ingest an entire diagnostic run. +From headless Workbench MCP, use `workbench_diagnostic_catalog`, `workbench_diagnostic_run`, and `workbench_diagnostic_summary` for layers 1–3 instead of globbing `reports/diagnostic-runs`. Those tools return sparse JSON only and do not dump `events.jsonl`, traces, or screenshot bytes. + ## Visual evidence rules - Never open a PNG merely to determine whether two screenshots differ. @@ -198,7 +249,7 @@ See `docs/ai-run-configuration.md` for the full profile/RunVar contract and `doc ## Pickleball syntax documentation -The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Use `docs/README.md` to select the relevant guide. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: +The exported `docs/` tree is the version-matched reference for all supported Pickleball behavior and syntax. Open a specific guide when the live loop or a diagnostic layer requires that topic; do not start by reading `docs/README.md` as a dump. Its links to the working consumer resolve into the exported `maven-consumer-project/` reference snapshot. In particular: - dynamic Gherkin/action/assertion syntax — `docs/dynamic-steps.md`; - element vocabulary/selectors — `docs/custom-element-definitions.md`; @@ -217,7 +268,7 @@ Do not guess Pickleball syntax when the version-matched guide or executable cons ## Human-readable consumer guidance -Use `docs/consumer-project.md` for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Use `docs/README.md` to navigate the complete bundled documentation. Human readers can open files under `maven-consumer-project/` directly in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples linked from those guides. +Use `docs/consumer-project.md` on demand for the Maven consumer layout, local test site, common tag entry points, diagnostic usage, and example commands. Human readers can start with `docs/README.md` and open files under `maven-consumer-project/` in the IDE to inspect the version-matched working features, configuration, calls, data, runner, and test-site examples. Agents should not treat those as first-read. ## When the core Pickleball repository is also present 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 4633e07b..ace01d46 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 @@ -26,11 +26,11 @@ 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. 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. That guide's tool chooser is the agent path: headless Workbench MCP (`mcp .`), one bounded diagnostic `mvn test`, 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. ## Version-matched reference snapshot -`export-guidance` also materializes a curated, read-only snapshot of the canonical Pickleball Maven consumer under `.pickleball/maven-consumer-project/`. It is intended for both human readers and AI agents that need concrete working examples in addition to prose documentation. +`export-guidance` also materializes a curated, read-only snapshot of the canonical Pickleball Maven consumer under `.pickleball/maven-consumer-project/`. It is a version-matched **reference** of Pickleball's own example consumer for on-demand lookup, not a sandbox and not the consumer project under test. `export-guidance` does not copy the current consumer's own features into `.pickleball` for testing. The snapshot includes: @@ -42,7 +42,7 @@ The snapshot includes: - static local test-site resources; and - the committed shared/local `profiles*.yaml` and `pickleball*.properties` examples. -It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` and `.github/copilot-instructions.md` bridges, internal Java verification classes, and maintainer-only `_local2` files. It is reference material, not another consumer project to edit or run. Make changes in the real consumer project; a future guidance export may replace every managed file in this snapshot. +It intentionally excludes Maven wrappers, Git/IDE/generated artifacts, the consumer `AGENTS.md` and `.github/copilot-instructions.md` bridges, internal Java verification classes, and maintainer-only `_local2` files. It is reference material, not another consumer project to copy, edit, or run. Make changes in the real consumer project; a future guidance export may replace every managed file in this snapshot. `.pickleball/workbench/live/classes` is a compiled worker overlay, not an editor. ## Purpose @@ -77,20 +77,20 @@ or use the included wrappers: ## Launch the dependency-matched Workbench -The test-scoped Pickleball dependency already contains its controller-only Workbench payload. Start the launcher from the resolved test classpath without finding a Maven cache entry or declaring a second version: +The test-scoped Pickleball dependency already contains its controller-only Workbench payload. Consumer AI agents start the **headless MCP** launcher from the resolved test classpath: ```bash ./mvnw -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ -Dexec.classpathScope=test \ - "-Dexec.args=ui ." + "-Dexec.args=mcp ." ``` ```powershell -.\mvnw.cmd -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=ui ." +.\mvnw.cmd -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -The launcher verifies and extracts the opaque payload beneath `.pickleball/workbench/controller//`, then creates a separate Workbench JVM. Workbench captures this project's compiled outputs and effective test runtime before creating a separate worker JVM. Only the worker loads the consumer-resolved Pickleball runtime; the Workbench artifact and process contain no core implementation. UI and MCP both use the same controller/live session. See `docs/pickleball-workbench.md` for commands, lifecycle, protocol compatibility, and isolation checks. +Humans who want the Swing player can pass `ui .` instead. Agents for this release should not use the GUI, `ui .`, or `attach.json` as their path. The launcher verifies and extracts the opaque payload beneath `.pickleball/workbench/controller//`, then creates a separate Workbench JVM. Workbench captures this project's compiled outputs and effective test runtime before creating a separate worker JVM. Only the worker loads the consumer-resolved Pickleball runtime; the Workbench artifact and process contain no core implementation. See `docs/pickleball-workbench.md` for commands, lifecycle, protocol compatibility, and isolation checks. The live-loop order lives in `.pickleball/AGENT-GUIDE.md`. Runner defaults include: @@ -152,6 +152,12 @@ 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: + +```bash +mvn test -Dpkb_runvars="pkb_tags=@the-failing-tag, pkb_name=The failing scenario, pkb_browser=CHROME_HEADLESS, pkb_reportingmode=diagnostic, pkb_loglevel=warn, pkb_reportretention=failed" +``` + The consumer `pom.xml` also defines Maven profiles such as: ```bash @@ -260,4 +266,4 @@ When evidence supports a bounded rerun: - Port `8765` must be available for the example test server. - Nested README/AGENTS files are minimal adapters; detailed guidance and version-matched reference examples are owned by Pickleball core and exported from the dependency. -Use `docs/README.md` for the complete version-matched Pickleball syntax/documentation map and `maven-consumer-project/` for the corresponding working reference files. +Human readers can use `docs/README.md` for the complete version-matched Pickleball syntax/documentation map and `maven-consumer-project/` for the corresponding working reference files. Agents should not treat those as first-read. 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 8df90b18..950c8f65 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 @@ -43,20 +43,22 @@ The normal `tools.dscode:pickleball:` test dependency already carries t 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: +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. + +Consumer AI agents use headless 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=ui ." + "-Dexec.args=mcp ." ``` ```powershell -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=ui ." +mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java "-Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher" "-Dexec.classpathScope=test" "-Dexec.args=mcp ." ``` -With no launcher arguments, `ui` and the current directory are selected automatically. Other Workbench commands are forwarded in the same form, for example `"-Dexec.args=sync ."` or `"-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 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: @@ -64,7 +66,7 @@ Gradle consumers can expose the same dependency-owned launcher without resolving tasks.register('pickleballWorkbench', JavaExec) { classpath = sourceSets.test.runtimeClasspath mainClass = 'tools.dscode.launcher.PickleballWorkbenchLauncher' - args 'ui', projectDir.absolutePath + args 'mcp', projectDir.absolutePath // consumer agents; pass 'ui' for the Swing player } ``` @@ -99,7 +101,15 @@ $workbenchJar = ".\pickleball-workbench\build\libs\pickleball-workbench-.jar mcp ` stays stdio JSON-RPC only. That client may hold the lease without a banner. Save is still a distinct explicit tool and never an implicit write. - -From `maven-consumer-project`: - -```bash -mvn -q org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ - -Dexec.mainClass=tools.dscode.launcher.PickleballWorkbenchLauncher \ - -Dexec.classpathScope=test \ - "-Dexec.args=ui ." -``` +Headless `java -jar pickleball-workbench-.jar mcp ` stays stdio JSON-RPC only. That is the consumer-agent path. That client may hold the lease without a banner. Save is still a distinct explicit tool and never an implicit write. -Then point the agent at `.pickleball/workbench/attach.json`. Do not launch a second `mcp` process against the same live UI session. +A human-watched UI session is optional and separate. From `maven-consumer-project`, a person may start `ui .` and then a watcher can join `.pickleball/workbench/attach.json`. Do not launch a second `mcp` process against the same live UI session, and do not treat that attach file as the default agent path. ## MCP stdio -Start the lightweight non-Spring MCP server for a synchronized consumer project: +Start the lightweight non-Spring MCP server for a consumer project. This is the consumer-agent path: ```powershell java -jar $workbenchJar mcp ".\maven-consumer-project" ``` +Or, from a Maven consumer test classpath, `"-Dexec.args=mcp ."`. Do not document or use the Swing GUI 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. ### Stdout contract @@ -345,6 +350,8 @@ workbench_worker_stop workbench_worker_status ``` +`workbench_sync` uses the skip / resources-only / full rules above. Live buffer edits do not require it. + Live runtime, Mapping, and watched-agent control: ```text @@ -391,6 +398,14 @@ workbench_step_override_remove workbench_step_override_clear ``` +Sparse diagnostic readers (do not glob `reports/diagnostic-runs`; these return JSON only and do not dump events, traces, or PNG bytes): + +```text +workbench_diagnostic_catalog +workbench_diagnostic_run +workbench_diagnostic_summary +``` + `workbench_step_override_compile` sends the Java source template to the consumer worker. The source must contain `{{CLASS_NAME}}`; worker-side Pickleball remains responsible for compilation, generated classloaders, matching, replacement, captures, and execution. Mutating live tools require the agent control lease. `workbench_request_save` never writes the original feature until the human Allows it in the UI, or until the explicit stdio tool call itself is the headless approval. Deny, Take control, and an unsavable demo buffer leave the file unchanged.