From 803b1eefc52ab2c882a8a923be7908d36b0a4589 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 23 Jul 2026 01:16:00 -0700 Subject: [PATCH 1/2] Strengthen Beta Release Validation --- README.md | 2 +- VERSION.toml | 2 +- deploy/install.sh | 3 +- docker-bake.hcl | 2 +- documentation/architecture/testing.md | 2 +- documentation/contribute/releases.md | 2 +- documentation/models/offline.md | 2 +- documentation/models/run-with-heartwood.md | 3 + documentation/platforms/carina.md | 9 ++- documentation/platforms/containers.md | 14 ++-- documentation/platforms/native-linux.md | 2 +- documentation/platforms/terra.md | 16 +++-- documentation/reference/gpu-compatibility.md | 7 +- .../skills/omop-cohort-summary/SKILL.md | 2 +- .../skills/omop-cohort-summary/metadata.json | 2 +- images/generic/scripts/coding_agent_e2e.sh | 46 +++++++++---- .../scripts/verify_coding_agent_e2e.py | 27 ++++++-- packages/adapters/pyproject.toml | 2 +- .../src/heartwood/adapters/__init__.py | 2 +- .../src/heartwood/adapters/conformance.py | 2 +- packages/adapters/tests/test_conformance.py | 2 +- packages/audit/pyproject.toml | 2 +- .../audit/src/heartwood/audit/__init__.py | 2 +- packages/cli/pyproject.toml | 2 +- packages/cli/src/heartwood/cli/__init__.py | 11 +-- packages/cli/src/heartwood/cli/_launch.py | 15 ++++ packages/cli/tests/test_cli.py | 2 +- packages/cli/tests/test_launch.py | 39 +++++++++++ packages/compliance/pyproject.toml | 2 +- .../src/heartwood/compliance/__init__.py | 2 +- .../tests/test_coding_agent_qualification.py | 69 ++++++++++++++++++- .../compliance/tests/test_container_assets.py | 12 +++- .../tests/test_release_governance.py | 4 +- packages/core-adapter/pyproject.toml | 2 +- .../src/heartwood/core_adapter/__init__.py | 2 +- packages/detector/pyproject.toml | 2 +- .../src/heartwood/detector/__init__.py | 2 +- packages/fixtures/pyproject.toml | 2 +- .../src/heartwood/fixtures/__init__.py | 2 +- packages/gateway/pyproject.toml | 2 +- packages/model-policy/pyproject.toml | 2 +- .../src/heartwood/model_policy/__init__.py | 2 +- packages/notebook/pyproject.toml | 2 +- .../src/heartwood/notebook/__init__.py | 2 +- packages/schemas/pyproject.toml | 2 +- .../schemas/src/heartwood/schemas/__init__.py | 2 +- packages/schemas/tests/test_schema_records.py | 4 +- packages/session/pyproject.toml | 2 +- .../session/src/heartwood/session/__init__.py | 2 +- packages/skills/pyproject.toml | 2 +- packages/webui/package-lock.json | 4 +- packages/webui/package.json | 2 +- skills/verified/aggregate-export/SKILL.md | 2 +- .../verified/aggregate-export/metadata.json | 2 +- skills/verified/baseline-model/SKILL.md | 2 +- skills/verified/baseline-model/metadata.json | 2 +- skills/verified/omop-cohort-summary/SKILL.md | 2 +- .../omop-cohort-summary/metadata.json | 2 +- uv.lock | 26 +++---- 59 files changed, 290 insertions(+), 103 deletions(-) diff --git a/README.md b/README.md index d4275e6f..a75a6d87 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ docker run --rm -it \ --env HOME=/tmp \ -p 127.0.0.1:8767:8767 \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10 \ heartwood --interface web --host 0.0.0.0 ``` diff --git a/VERSION.toml b/VERSION.toml index a464762a..f85c9a0c 100644 --- a/VERSION.toml +++ b/VERSION.toml @@ -2,4 +2,4 @@ # SPDX-FileCopyrightText: 2026 Stanford University and the project authors (see CONTRIBUTORS.md) # SPDX-License-Identifier: MIT -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" diff --git a/deploy/install.sh b/deploy/install.sh index df4c5e8a..e3d7e594 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -478,7 +478,8 @@ fi replace_symlink "${current_target}" "${root}/current" installation_succeeded="true" +cleanup +trap - EXIT stage "Installation complete" -rm -rf "${installer_state}" printf 'Installed %s in %d seconds.\n' "${release_version}" "${SECONDS}" printf 'Add %s to PATH, then run: heartwood doctor\n' "${root}/bin" diff --git a/docker-bake.hcl b/docker-bake.hcl index d9d8d597..844a5d03 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -17,7 +17,7 @@ variable "GIT_SHA" { } variable "HEARTWOOD_VERSION" { - default = "0.2.0-beta.9" + default = "0.2.0-beta.10" } variable "TERRA_BASE_IMAGE" { diff --git a/documentation/architecture/testing.md b/documentation/architecture/testing.md index f7ae9997..9dac207e 100644 --- a/documentation/architecture/testing.md +++ b/documentation/architecture/testing.md @@ -28,7 +28,7 @@ Qualification profiles select external model weights and runtime arguments again An optional protected self-hosted GPU job runs the same model qualification used on managed platforms when an eligible runner is configured. Without GPU hardware, CI does not claim successful CUDA initialization or GPU model loading. -The shared coding-agent acceptance test performs direct model inference and then drives the real Heartwood gateway and OpenHands adapter through a structured terminal proposal, grouped approval, synthetic file modification, independent file verification, fresh-process replay, and hash-chain-verified audit export. +The shared coding-agent acceptance test performs direct model inference and then drives the real Heartwood gateway and OpenHands adapter through structured terminal proposals, grouped approval and rejection, synthetic file modification, byte-exact independent verification, proof that the rejected action did not execute, fresh-process replay, and hash-chain-verified audit export. It emits a machine-readable qualification record containing the exact runtime, model revision, GPU, driver, context, tensor parallelism, server parser, and agent tool mode. The CPU capable-model job and GPU qualification wrapper use this same acceptance contract instead of maintaining separate agent scenarios. diff --git a/documentation/contribute/releases.md b/documentation/contribute/releases.md index ee4afb69..5f4f2d7e 100644 --- a/documentation/contribute/releases.md +++ b/documentation/contribute/releases.md @@ -23,7 +23,7 @@ The workflow verifies immutable container candidates, builds and tests native as ## Stable and Preview Documentation A stable version updates the `stable` alias and the documentation root. -A prerelease such as `0.2.0-beta.9` updates the `preview` alias without replacing the stable root. +A prerelease such as `0.2.0-beta.10` updates the `preview` alias without replacing the stable root. The version store is deployed to GitHub Pages and retains immutable version paths. Publishing the same version with different content is rejected. diff --git a/documentation/models/offline.md b/documentation/models/offline.md index 2c919812..de026d89 100644 --- a/documentation/models/offline.md +++ b/documentation/models/offline.md @@ -32,7 +32,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10 \ heartwood ``` diff --git a/documentation/models/run-with-heartwood.md b/documentation/models/run-with-heartwood.md index 2b181386..b7d2a09e 100644 --- a/documentation/models/run-with-heartwood.md +++ b/documentation/models/run-with-heartwood.md @@ -8,6 +8,7 @@ SPDX-License-Identifier: MIT Heartwood images and the generic Linux package include managed inference software but no model weights. After a supported model is selected and downloaded or imported, the normal `heartwood` command verifies the files, plans context against available memory, starts the runtime, waits for readiness, and then opens the requested interface. +Downloading and verification are separate costs: a cached model avoids network transfer but Heartwood still verifies its immutable source and file hashes before use. ## Download and Start @@ -62,6 +63,8 @@ heartwood runtime start --task-profile powerful --partition dev --time 01:00:00 On Carina, Heartwood prints the complete Slurm request and asks before allocating a GPU. On provisioned Terra compute, it uses the attached resources without submitting a scheduler request. Model download and scheduler allocation require separate confirmation. +An advanced `runtime start --dry-run` may show a compatible recommendation when the project has no selected model, but it never persists that recommendation. +Complete the normal `heartwood` setup flow before starting or allocating the runtime. ## Stop the Runtime diff --git a/documentation/platforms/carina.md b/documentation/platforms/carina.md index 471b1a2b..40da0059 100644 --- a/documentation/platforms/carina.md +++ b/documentation/platforms/carina.md @@ -41,7 +41,7 @@ Do not use a shared project root itself as the Heartwood project. ```bash cd heartwood-installation curl --fail --location --remote-name \ - https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.9/heartwood-installer + https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.10/heartwood-installer chmod 700 heartwood-installer ./heartwood-installer --platform carina export PATH="$PWD/bin:$PATH" @@ -81,6 +81,8 @@ Those current Stanford terms, not Heartwood platform detection, determine data e Choose **Run with Heartwood**, select a catalog model or another public Hugging Face repository, and review the download and resource plan. Model files are stored under the project's `.heartwood/models/`, not the installation directory. +Model setup verifies every file before it persists the selection. +On project storage, verification of a large existing cache can take several minutes even when no network download is needed. When you start Heartwood with a selected Heartwood-managed model, it inspects the GPU-capable Slurm partitions, available L40S count, GPU memory, CPU and RAM limits, existing model cache, and requested capability tier. It then prints the strongest compatible qualified model, expected download and startup range, and complete `srun` request. @@ -121,11 +123,16 @@ Preview a particular capability tier without downloading or allocating: heartwood runtime start --task-profile powerful --dry-run ``` +This preview is a recommendation only. +It does not select or download the model. +Run `heartwood` and complete model setup before using `heartwood runtime start` without `--dry-run`; Heartwood otherwise stops before requesting an allocation. + `auto` prefers **Powerful** on Carina and falls back to the strongest qualified configuration that fits one available allocation. Use `--task-profile standard`, `powerful`, or `maximum` when the task has a known resource envelope. The `--gpus` option is an advanced constraint and must match a catalog configuration that was qualified at that tensor-parallel size. Heartwood scopes model caches to the project, waits up to ten minutes by default, and reports the current stage and elapsed startup time every 15 seconds. +The installer reports completion only after temporary installation state and locks have been removed. For scripted deployment, `--yes-download` and `--yes-request-allocation` are separate explicit approvals; normal interactive use should retain both prompts. ## Review, Exit, and Return diff --git a/documentation/platforms/containers.md b/documentation/platforms/containers.md index 09626153..3ef9b5bf 100644 --- a/documentation/platforms/containers.md +++ b/documentation/platforms/containers.md @@ -27,7 +27,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10 \ heartwood ``` @@ -44,7 +44,7 @@ docker run --rm -it \ --env HOME=/tmp \ -p 127.0.0.1:8767:8767 \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9 \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10 \ heartwood --interface web --host 0.0.0.0 ``` @@ -63,7 +63,7 @@ docker run --rm -it \ --user "$(id -u):$(id -g)" \ --env HOME=/tmp \ -v "$PWD:/workspace" \ - ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-gpu-nvidia \ + ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-gpu-nvidia \ heartwood ``` @@ -75,10 +75,10 @@ Review the [GPU compatibility matrix](../reference/gpu-compatibility.md) before Use immutable release tags for research work: -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9` — standard AMD64/ARM64 image; -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-gpu-nvidia` — NVIDIA GPU image; -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-terra` — Terra CPU image; and -- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-terra-gpu-nvidia` — Terra NVIDIA image. +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10` — standard AMD64/ARM64 image; +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-gpu-nvidia` — NVIDIA GPU image; +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-terra` — Terra CPU image; and +- `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-terra-gpu-nvidia` — Terra NVIDIA image. The moving `edge` tags represent current `main` and are intended for development, not reproducible analyses. Release publication verifies candidate digests and manifest shape before creating version tags. diff --git a/documentation/platforms/native-linux.md b/documentation/platforms/native-linux.md index fef9d31e..2de13e0a 100644 --- a/documentation/platforms/native-linux.md +++ b/documentation/platforms/native-linux.md @@ -48,7 +48,7 @@ mkdir -m 700 heartwood-installation cd heartwood-installation curl --fail --location --remote-name \ - https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.9/heartwood-installer + https://github.com/SchmiedmayerLab/heartwood/releases/download/0.2.0-beta.10/heartwood-installer chmod 700 heartwood-installer ./heartwood-installer --platform generic export PATH="$PWD/bin:$PATH" diff --git a/documentation/platforms/terra.md b/documentation/platforms/terra.md index d5cf7fd5..303f10de 100644 --- a/documentation/platforms/terra.md +++ b/documentation/platforms/terra.md @@ -31,9 +31,10 @@ Open the workspace's Jupyter Cloud Environment settings and configure the enviro 1. Select **Customize**, then choose **Custom Environment** under application configuration. 2. Select the CPU and memory combination from the table below. -3. Enter the corresponding container image. -4. Enable the GPU, when required, and verify the GPU type and count. -5. Set auto-pause and review every value before selecting **Create**. +3. Enable the GPU, when required, and verify the GPU type and count. +4. Set the persistent-disk size and auto-pause interval. +5. Enter the corresponding container image last. +6. Review every value before selecting **Create**. Terra can reset the image or GPU selection when the CPU choice changes, so set compute resources first and verify the complete form before creation. @@ -41,8 +42,8 @@ Use one of these combinations: | Model Route | Image | Practical Starting Point | |---|---|---| -| Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | -| Qualified managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.9-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, two T4 GPUs with 16 GB each, 200 GB persistent disk | +| Research environment or hosted service | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-terra` | 8 CPUs, 30 GB RAM, 50 GB persistent disk | +| Qualified managed GPU inference | `ghcr.io/schmiedmayerlab/heartwood:0.2.0-beta.10-terra-gpu-nvidia` | 32 CPUs, 120 GB RAM, two T4 GPUs with 16 GB each, 200 GB persistent disk | A hosted model is the shortest first run. Use the GPU image for a capable model managed inside the Terra environment. @@ -63,6 +64,10 @@ Larger GPU memory can enable context capacities above 32K when the model support See [Choose a Heartwood-Managed Model](../models/choose-managed.md) for download and resource estimates and [GPU Compatibility](../reference/gpu-compatibility.md) for exact runtime combinations. Retain the persistent disk when replacing compute and copy valuable results to workspace storage. +Before downloading a managed model, open a terminal and run `df -h /home/jupyter`. +The disk needs the model's recommended free space in addition to existing notebooks, project files, and `.heartwood/` state. +If the retained disk is too small, preserve important files, delete only the Cloud Environment, and recreate it with a larger persistent disk. +For an existing project that already contains model files or other analyses, 250 GB is a safer managed-inference starting point than the 200 GB fresh-project minimum. See [Starting and Customizing Your Jupyter App](https://support.terra.bio/hc/en-us/articles/5075814468379-Starting-and-customizing-your-Jupyter-app). ## Create and Verify the Environment @@ -173,6 +178,7 @@ Deleting the persistent disk removes `.heartwood/` and project files stored only - If a Heartwood browser URL returns **401** or **404**, use the terminal or notebook interface; browser access is not supported on Terra. - If `import heartwood` fails in a notebook, switch the notebook kernel to **Python 3 (Heartwood)** and restart the kernel. - If a model download stops, rerun Heartwood from the same project; verified files in `.heartwood/models/` are reused. +- If Heartwood reports insufficient persistent storage, run `df -h /home/jupyter`, retain needed files, and recreate only the Cloud Environment with a larger disk. - If model startup is slow or fails, compare the printed model plan with attached RAM, GPU memory, and persistent-disk space, then inspect `.heartwood/logs/local-model.log` from the same project. - If Heartwood reports an unsupported P4, P100, or V100, delete and recreate the Cloud Environment with a T4 while retaining the persistent disk; do not replace the released vLLM or PyTorch packages in place. - If the GPU is not detected, confirm that the GPU image and GPU were selected together, then run `nvidia-smi` and `heartwood doctor` from the project terminal. diff --git a/documentation/reference/gpu-compatibility.md b/documentation/reference/gpu-compatibility.md index ce660f09..f1a3ffb5 100644 --- a/documentation/reference/gpu-compatibility.md +++ b/documentation/reference/gpu-compatibility.md @@ -75,9 +75,10 @@ The acceptance test must establish all of the following: 2. OpenHands uses the catalog-qualified tool mode: native structured tools for supported parsers or its prompt-conversion path for models that do not reliably emit native calls; 3. Heartwood presents the complete action set for approval; 4. approval executes the proposed operation and modifies only the synthetic project; -5. an independent check verifies the exact file result; -6. a fresh process replays the session; and -7. audit export validates event coverage, hash-chain integrity, and content scrubbing. +5. a second proposed action set is rejected and does not modify the project; +6. an independent check verifies the exact file bytes; +7. a fresh process replays both decisions and the approved result; and +8. audit export validates event coverage, hash-chain integrity, and content scrubbing. The result records the GPU model, count, memory, driver, runtime versions, model revision, context size, tensor parallelism, server parser, and agent tool mode. Not-tested configurations are not added to the managed catalog. diff --git a/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md b/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md index c6a46024..01e477fe 100644 --- a/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md +++ b/fixtures/synthetic/skills/omop-cohort-summary/SKILL.md @@ -10,7 +10,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.9" + heartwood.version: "0.2.0-beta.10" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/fixtures/synthetic/skills/omop-cohort-summary/metadata.json b/fixtures/synthetic/skills/omop-cohort-summary/metadata.json index 905702eb..445e978b 100644 --- a/fixtures/synthetic/skills/omop-cohort-summary/metadata.json +++ b/fixtures/synthetic/skills/omop-cohort-summary/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.9", + "heartwood.version": "0.2.0-beta.10", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/images/generic/scripts/coding_agent_e2e.sh b/images/generic/scripts/coding_agent_e2e.sh index 35b7f8d3..4dc4daea 100755 --- a/images/generic/scripts/coding_agent_e2e.sh +++ b/images/generic/scripts/coding_agent_e2e.sh @@ -45,6 +45,8 @@ inference="${project}/qualification-inference.json" command_timeout="${HEARTWOOD_COMMAND_TIMEOUT:-900}" runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" cohort_path="${project}/cohort-summary.json" +exact_path="${project}/heartwood-exact-output.txt" +rejected_path="${project}/heartwood-rejected-output.txt" events_path="${workspace}/${session_id}/events.jsonl" audit_path="${state_root}/audit-export.jsonl" @@ -67,7 +69,13 @@ export OPENHANDS_SUPPRESS_BANNER=1 rm -rf "${project}/input" "${state_root}" mkdir -p "${project}/input" -rm -f "${cohort_path}" "${transcript}" "${replay}" "${report}" +rm -f \ + "${cohort_path}" \ + "${exact_path}" \ + "${rejected_path}" \ + "${transcript}" \ + "${replay}" \ + "${report}" cp "${runtime_root}/fixtures/synthetic/omop-like/"*.csv "${project}/input/" cd "${project}" @@ -106,16 +114,8 @@ run_heartwood() { timeout "${command_timeout}" "${heartwood_cli}" "$@" } -run_heartwood models refresh heartwood | tee -a "${transcript}" -run_heartwood models connect heartwood heartwood-managed-runtime | tee -a "${transcript}" -run_heartwood models validate heartwood | tee -a "${transcript}" -run_heartwood actions set ask-every-time | tee -a "${transcript}" -run_heartwood --session-id "${session_id}" \ - --prompt "Call the terminal tool to execute this exact command: ${heartwood_python} ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ - | tee -a "${transcript}" - -for _ in 1 2 3 4; do - pending_id="$("${heartwood_python}" - "${events_path}" <<'PY' +pending_action_id() { + "${heartwood_python}" - "${events_path}" <<'PY' import json import sys from pathlib import Path @@ -134,13 +134,35 @@ pending = [ ] print(pending[-1] if pending else "") PY -)" +} + +run_heartwood models refresh heartwood | tee -a "${transcript}" +run_heartwood models connect heartwood heartwood-managed-runtime | tee -a "${transcript}" +run_heartwood models validate heartwood | tee -a "${transcript}" +run_heartwood actions set ask-every-time | tee -a "${transcript}" +run_heartwood --session-id "${session_id}" \ + --prompt "Call the terminal tool to execute this exact command: ${heartwood_python} ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && printf 'heartwood-agent-exact-ok\\n' > heartwood-exact-output.txt && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ + | tee -a "${transcript}" + +for _ in 1 2 3 4; do + pending_id="$(pending_action_id)" if [[ -z "${pending_id}" ]]; then break fi run_heartwood --session-id "${session_id}" allow | tee -a "${transcript}" done +run_heartwood --session-id "${session_id}" \ + --prompt "Call the terminal tool once to execute exactly: printf 'this-action-must-remain-rejected\\n' > heartwood-rejected-output.txt. Do not call another tool." \ + | tee -a "${transcript}" + +pending_id="$(pending_action_id)" +if [[ -z "${pending_id}" ]]; then + echo "coding-agent rejection check did not produce a pending action" >&2 + exit 1 +fi +run_heartwood --session-id "${session_id}" reject | tee -a "${transcript}" + run_heartwood --session-id "${session_id}" replay | tee "${replay}" run_heartwood --session-id "${session_id}" audit export \ --output "${audit_path}" | tee -a "${transcript}" diff --git a/images/generic/scripts/verify_coding_agent_e2e.py b/images/generic/scripts/verify_coding_agent_e2e.py index 861149b2..17dcc969 100644 --- a/images/generic/scripts/verify_coding_agent_e2e.py +++ b/images/generic/scripts/verify_coding_agent_e2e.py @@ -78,8 +78,11 @@ def verify_run( for event in events if event.kind == "confirmation.resolved" } - if decisions != {"approved"}: - raise ValueError(f"coding-agent actions were not approved: {sorted(decisions)}") + if decisions != {"approved", "denied"}: + raise ValueError( + "coding-agent qualification must record one approved and one denied action set: " + f"{sorted(decisions)}" + ) tool_executions = [event for event in events if event.kind == "tool.execution.recorded"] terminal_executions = [ @@ -140,14 +143,26 @@ def verify_run( raise ValueError("coding-agent artifact contains row-level output") if cohort["export_guard"].get("exportable") is not True: raise ValueError("coding-agent artifact unexpectedly failed its count floor") + if not artifact_path.read_bytes().endswith(b"\n"): + raise ValueError("coding-agent artifact does not end with a newline") + exact_path = artifact_path.with_name("heartwood-exact-output.txt") + if not exact_path.is_file() or exact_path.read_bytes() != b"heartwood-agent-exact-ok\n": + raise ValueError("coding-agent exact-content artifact is incorrect") + rejected_path = artifact_path.with_name("heartwood-rejected-output.txt") + if rejected_path.exists(): + raise ValueError("coding-agent rejected action modified the project") inference = json.loads(inference_path.read_text(encoding="utf-8")) if inference.get("content_nonempty") is not True: raise ValueError("direct model inference did not return content") replay = replay_path.read_text(encoding="utf-8") - if "Tool terminal exit=0" not in replay or "Action set approved" not in replay: - raise ValueError("fresh-process replay is missing the approved tool execution") + if ( + "Tool terminal exit=0" not in replay + or "Action set approved" not in replay + or "Action set denied" not in replay + ): + raise ValueError("fresh-process replay is missing the approved or denied action set") audit = AuditLog(audit_path) audit_events = audit.read() @@ -161,6 +176,8 @@ def verify_run( str(artifact_path.resolve().parent), "target-condition-concept-id", "Call the terminal tool", + "heartwood-agent-exact-ok", + "this-action-must-remain-rejected", ): if sensitive_value in audit_text: raise ValueError("audit export contains unsanitized task content") @@ -173,7 +190,9 @@ def verify_run( "model_loaded_and_inferred": True, "tool_call_proposed": True, "grouped_approval_recorded": True, + "grouped_rejection_recorded": True, "file_modified_and_verified": True, + "exact_content_verified": True, "fresh_process_replay_verified": True, "audit_export_verified": True, }, diff --git a/packages/adapters/pyproject.toml b/packages/adapters/pyproject.toml index 9b11f85d..c0d37cf0 100644 --- a/packages/adapters/pyproject.toml +++ b/packages/adapters/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-adapters" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Adapter service provider interfaces and conformance checks for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/adapters/src/heartwood/adapters/__init__.py b/packages/adapters/src/heartwood/adapters/__init__.py index a3b3bf8c..7454fd09 100644 --- a/packages/adapters/src/heartwood/adapters/__init__.py +++ b/packages/adapters/src/heartwood/adapters/__init__.py @@ -39,4 +39,4 @@ "assert_registry_adapter_conforms", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/adapters/src/heartwood/adapters/conformance.py b/packages/adapters/src/heartwood/adapters/conformance.py index 5c7422c7..19527d82 100644 --- a/packages/adapters/src/heartwood/adapters/conformance.py +++ b/packages/adapters/src/heartwood/adapters/conformance.py @@ -64,7 +64,7 @@ def assert_data_source_adapter_conforms( def assert_registry_adapter_conforms( adapter: RegistryAdapter, skill_id: str = "heartwood.synthetic.omop-cohort-summary", - version: str = "0.2.0-beta.9", + version: str = "0.2.0-beta.10", ) -> None: """Assert the shared minimum contract for registry adapters.""" assert adapter.registry_id diff --git a/packages/adapters/tests/test_conformance.py b/packages/adapters/tests/test_conformance.py index 9973ca14..73566aa3 100644 --- a/packages/adapters/tests/test_conformance.py +++ b/packages/adapters/tests/test_conformance.py @@ -119,7 +119,7 @@ def resolve_skill(self, skill_id: str, version: str) -> SkillReference: def verify_skill(self, reference: SkillReference) -> RegistryVerification: """Verify the synthetic skill reference.""" return RegistryVerification( - verified=reference.version == "0.2.0-beta.9", + verified=reference.version == "0.2.0-beta.10", reason="synthetic fixture registry result", ) diff --git a/packages/audit/pyproject.toml b/packages/audit/pyproject.toml index b6f0ada0..bc96e043 100644 --- a/packages/audit/pyproject.toml +++ b/packages/audit/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-audit" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Hash-chained audit logging for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/audit/src/heartwood/audit/__init__.py b/packages/audit/src/heartwood/audit/__init__.py index edca2476..5da20140 100644 --- a/packages/audit/src/heartwood/audit/__init__.py +++ b/packages/audit/src/heartwood/audit/__init__.py @@ -18,4 +18,4 @@ "scrub_json_value", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/cli/pyproject.toml b/packages/cli/pyproject.toml index d2981916..268ce46d 100644 --- a/packages/cli/pyproject.toml +++ b/packages/cli/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-cli" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "The heartwood command-line interface — the primary interaction surface." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/cli/src/heartwood/cli/__init__.py b/packages/cli/src/heartwood/cli/__init__.py index a3e150f7..1267c1c2 100644 --- a/packages/cli/src/heartwood/cli/__init__.py +++ b/packages/cli/src/heartwood/cli/__init__.py @@ -72,7 +72,7 @@ __all__ = ["__version__", "main"] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" _PROG = "heartwood" @@ -99,9 +99,12 @@ def _bundled_path(relative: Path) -> Path: "stanford-ai-api-gateway": "stanford-ai-api-gateway", } _MODEL_DOWNLOAD_ACTIVITY = InteractionActivity( - label="Downloading and verifying the model", - waiting_label="Still downloading and verifying the model", - guidance="Large models can take several minutes. Keep this process running.", + label="Preparing and verifying the model", + waiting_label="Still preparing and verifying the model", + guidance=( + "Large downloads and full verification of existing model files can take several minutes. " + "Keep this process running." + ), ) _STARTUP_ACTIVITY = InteractionActivity( label="Checking the project and environment", diff --git a/packages/cli/src/heartwood/cli/_launch.py b/packages/cli/src/heartwood/cli/_launch.py index 127d6081..04983e96 100644 --- a/packages/cli/src/heartwood/cli/_launch.py +++ b/packages/cli/src/heartwood/cli/_launch.py @@ -119,6 +119,7 @@ class LaunchPlan: startup_seconds_max: int | None = None environment_notes: tuple[str, ...] = () download_required: bool = False + model_selected: bool = True def format(self) -> str: """Render the launch proposal without secrets.""" @@ -133,6 +134,11 @@ def format(self) -> str: "Catalog model: " f"{self.artifact_id if self.artifact_id is not None else 'not selected'}" ), + ( + "Model status: selected for this project" + if self.model_selected + else "Model status: recommendation only; complete setup before startup" + ), f"Runtime: {self.runtime if self.runtime is not None else 'not selected'}", ( f"Context capacity: up to {self.context_window:,} tokens" @@ -341,6 +347,7 @@ def build_launch_plan(options: LaunchOptions, env: Mapping[str, str]) -> LaunchP """Build a platform-specific launch plan without changing external state.""" platform_id = select_platform_adapter(env).adapter_id selection = _local_model_selection(options.project, env) + model_selected = selection is not None if selection is None: recommendation = _recommend_model(options, env, platform_id=platform_id) if recommendation is not None: @@ -404,6 +411,7 @@ def build_launch_plan(options: LaunchOptions, env: Mapping[str, str]) -> LaunchP and selection.catalog_source == "catalog" and not selection.model_root.exists() ), + model_selected=model_selected, ) @@ -443,6 +451,13 @@ def run_launch( "Run `heartwood models managed` for lower-resource and advanced options." ) return 64 + if not plan.model_selected: + print( + "\nThis plan is a resource recommendation, not a selected model. " + "Run `heartwood` to choose Run with Heartwood, or use `heartwood setup` " + "before requesting a download or GPU allocation." + ) + return 64 if plan.download_required: if not active_options.yes_download: try: diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index 1f46d9b9..b598d467 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -1327,7 +1327,7 @@ def download( assert "Heartwood model plan" in output assert "Runtime: CPU" in output assert "Recommended: 8 CPU cores" in output - assert "Downloading and verifying the model" in captured.err + assert "Preparing and verifying the model" in captured.err assert "Model files are ready:" in output assert "Run `heartwood` to continue setup or open Heartwood." in output assert calls == [ diff --git a/packages/cli/tests/test_launch.py b/packages/cli/tests/test_launch.py index 86d99592..b183ba04 100644 --- a/packages/cli/tests/test_launch.py +++ b/packages/cli/tests/test_launch.py @@ -22,6 +22,7 @@ from heartwood.cli._launch import ( LaunchConfigurationError, LaunchOptions, + LaunchPlan, LocalRuntimeSelection, _allocation_resources, _available_gpu_memory_bytes, @@ -460,6 +461,44 @@ def test_launch_reports_missing_selection_artifact_and_runtime( assert "vLLM executable is unavailable" in capsys.readouterr().out +def test_recommended_model_cannot_download_or_allocate_before_setup( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + options = _options(tmp_path, selected=False, yes_download=True, yes_request_allocation=True) + runner_called = False + + def runner(_command: Sequence[str]) -> int: + nonlocal runner_called + runner_called = True + return 0 + + plan = LaunchPlan( + platform_id="carina", + allocation_required=True, + allocation_command=("srun", "--pty", "heartwood"), + model_root=options.project.models_dir / "recommended-model", + state_root=options.project.state_root, + project_root=options.project.root, + runtime="vllm", + model_id="heartwood-managed-model", + artifact_id="recommended-model", + context_window=32_768, + download_required=True, + model_selected=False, + ) + monkeypatch.setattr("heartwood.cli._launch.build_launch_plan", lambda *_args: plan) + + assert run_launch(options, env={"HEARTWOOD_PLATFORM": "carina"}, run_fn=runner) == 64 + output = capsys.readouterr().out + assert "Model status: recommendation only" in output + assert "Run `heartwood` to choose Run with Heartwood" in output + assert not runner_called + assert plan.model_root is not None + assert not plan.model_root.exists() + + def test_launch_rejects_short_context_before_starting_runtime( tmp_path: Path, capsys: pytest.CaptureFixture[str], diff --git a/packages/compliance/pyproject.toml b/packages/compliance/pyproject.toml index f07e0486..c964abae 100644 --- a/packages/compliance/pyproject.toml +++ b/packages/compliance/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-compliance" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Synthetic-only reviewer packet and audit bundle generation for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/compliance/src/heartwood/compliance/__init__.py b/packages/compliance/src/heartwood/compliance/__init__.py index 997f6ad4..0405ce5e 100644 --- a/packages/compliance/src/heartwood/compliance/__init__.py +++ b/packages/compliance/src/heartwood/compliance/__init__.py @@ -12,4 +12,4 @@ __all__ = ["ReviewerPacket", "ReviewerPacketGenerator", "__version__"] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/compliance/tests/test_coding_agent_qualification.py b/packages/compliance/tests/test_coding_agent_qualification.py index 279271d7..a0fac3d7 100644 --- a/packages/compliance/tests/test_coding_agent_qualification.py +++ b/packages/compliance/tests/test_coding_agent_qualification.py @@ -82,7 +82,30 @@ def _acceptance_files(tmp_path: Path) -> tuple[Path, Path, Path, Path, Path]: "tool.execution.recorded", {"tool_name": "finish", "exit_code": 0}, ), - _event(6, "audit.export.recorded", {"scrubbed": True}), + _event( + 6, + "model_call.decision.recorded", + { + "decision": {"decision": "allow"}, + "model_profile": {"action_confirmation_mode": "always-confirm"}, + }, + ), + _event( + 7, + "tool_call.proposed", + {"tool_call_id": "tool-2", "tool_name": "terminal"}, + ), + _event( + 8, + "confirmation.requested", + {"request": {"tool_call_id": "tool-2"}}, + ), + _event( + 9, + "confirmation.resolved", + {"tool_call_id": "tool-2", "decision": "denied"}, + ), + _event(10, "audit.export.recorded", {"scrubbed": True}), ) events_path = tmp_path / "events.jsonl" events_path.write_text( @@ -111,12 +134,14 @@ def _acceptance_files(tmp_path: Path) -> tuple[Path, Path, Path, Path, Path]: "quality_checks": {"aggregate_only_output": True}, "export_guard": {"exportable": True}, } - ), + ) + + "\n", encoding="utf-8", ) + (tmp_path / "heartwood-exact-output.txt").write_bytes(b"heartwood-agent-exact-ok\n") replay_path = tmp_path / "replay.txt" replay_path.write_text( - "Action set approved (1 action)\nTool terminal exit=0\n", + "Action set approved (1 action)\nTool terminal exit=0\nAction set denied (1 action)\n", encoding="utf-8", ) inference_path = tmp_path / "inference.json" @@ -144,6 +169,44 @@ def test_coding_agent_qualification_verifies_complete_acceptance_evidence( assert summary["tool_execution_count"] == 2 assert cast(dict[str, bool], summary["checks"])["audit_export_verified"] is True + assert cast(dict[str, bool], summary["checks"])["exact_content_verified"] is True + assert cast(dict[str, bool], summary["checks"])["grouped_rejection_recorded"] is True + + +def test_coding_agent_qualification_rejects_inexact_or_rejected_output( + tmp_path: Path, +) -> None: + module = _module( + "verify_coding_agent_e2e_exact_content", + _root() / "images/generic/scripts/verify_coding_agent_e2e.py", + ) + verify = cast(Callable[..., dict[str, object]], module.verify_run) + events, audit, artifact, replay, inference = _acceptance_files(tmp_path) + exact = tmp_path / "heartwood-exact-output.txt" + + exact.write_bytes(b"heartwood-agent-exact-ok") + with pytest.raises(ValueError, match="exact-content artifact"): + verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) + + exact.write_bytes(b"heartwood-agent-exact-ok\n") + (tmp_path / "heartwood-rejected-output.txt").write_text( + "this-action-must-remain-rejected\n", + encoding="utf-8", + ) + with pytest.raises(ValueError, match="rejected action modified"): + verify( + events_path=events, + audit_path=audit, + artifact_path=artifact, + replay_path=replay, + inference_path=inference, + ) def test_coding_agent_qualification_requires_successful_completion( diff --git a/packages/compliance/tests/test_container_assets.py b/packages/compliance/tests/test_container_assets.py index 79580497..d5428ab7 100644 --- a/packages/compliance/tests/test_container_assets.py +++ b/packages/compliance/tests/test_container_assets.py @@ -219,7 +219,7 @@ def test_runtime_image_sets_the_release_version_label() -> None: assert "ARG HEARTWOOD_VERSION=development" in dockerfile assert 'org.opencontainers.image.version="${HEARTWOOD_VERSION}"' in dockerfile assert 'variable "HEARTWOOD_VERSION"' in bake - assert 'default = "0.2.0-beta.9"' in bake + assert 'default = "0.2.0-beta.10"' in bake assert bake.count('HEARTWOOD_VERSION = "${HEARTWOOD_VERSION}"') == 2 @@ -529,7 +529,15 @@ def test_coding_agent_qualification_finds_cli_beside_selected_python(tmp_path: P path_bin = tmp_path / "path-bin" path_bin.mkdir() python = runtime_bin / "python" - python.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8") + python.write_text( + """#!/usr/bin/env bash +if [[ "${1:-}" == "-" && "${2:-}" == */events.jsonl ]]; then + printf 'synthetic-pending-action\n' +fi +exit 0 +""", + encoding="utf-8", + ) python.chmod(0o700) marker = tmp_path / "heartwood-invocations.txt" diff --git a/packages/compliance/tests/test_release_governance.py b/packages/compliance/tests/test_release_governance.py index dd2047cf..33c25067 100644 --- a/packages/compliance/tests/test_release_governance.py +++ b/packages/compliance/tests/test_release_governance.py @@ -189,9 +189,9 @@ def test_prerelease_sources_use_semver_and_python_lock_uses_pep440( assert _release_verifier().source_version_errors(tmp_path, version) == [] - skill_metadata.write_text('{"heartwood.version": "0.2.0-beta.9"}\n', encoding="utf-8") + skill_metadata.write_text('{"heartwood.version": "0.2.0-beta.10"}\n', encoding="utf-8") assert ( - "skills/verified/example/metadata.json: 0.2.0-beta.9" + "skills/verified/example/metadata.json: 0.2.0-beta.10" in _release_verifier().source_version_errors(tmp_path, version) ) diff --git a/packages/core-adapter/pyproject.toml b/packages/core-adapter/pyproject.toml index 1ceb282d..4f79d494 100644 --- a/packages/core-adapter/pyproject.toml +++ b/packages/core-adapter/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-core-adapter" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Core harness orchestration for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/core-adapter/src/heartwood/core_adapter/__init__.py b/packages/core-adapter/src/heartwood/core_adapter/__init__.py index 746bb039..ebc61444 100644 --- a/packages/core-adapter/src/heartwood/core_adapter/__init__.py +++ b/packages/core-adapter/src/heartwood/core_adapter/__init__.py @@ -35,4 +35,4 @@ "__version__", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/detector/pyproject.toml b/packages/detector/pyproject.toml index 405eb8a4..19941c33 100644 --- a/packages/detector/pyproject.toml +++ b/packages/detector/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-detector" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Deterministic, propose-not-commit environment and dataset detection for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/detector/src/heartwood/detector/__init__.py b/packages/detector/src/heartwood/detector/__init__.py index 55183ece..bb18b7d4 100644 --- a/packages/detector/src/heartwood/detector/__init__.py +++ b/packages/detector/src/heartwood/detector/__init__.py @@ -27,4 +27,4 @@ "platform_detection_evidence", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/fixtures/pyproject.toml b/packages/fixtures/pyproject.toml index e93b29c5..b60ce568 100644 --- a/packages/fixtures/pyproject.toml +++ b/packages/fixtures/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-fixtures" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Synthetic fixture linting for Heartwood tests and replay artifacts." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/fixtures/src/heartwood/fixtures/__init__.py b/packages/fixtures/src/heartwood/fixtures/__init__.py index 27995ca5..494e2ff0 100644 --- a/packages/fixtures/src/heartwood/fixtures/__init__.py +++ b/packages/fixtures/src/heartwood/fixtures/__init__.py @@ -12,4 +12,4 @@ __all__ = ["FixtureFinding", "__version__", "lint_fixture_tree", "main"] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/gateway/pyproject.toml b/packages/gateway/pyproject.toml index a967b378..788e593b 100644 --- a/packages/gateway/pyproject.toml +++ b/packages/gateway/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-gateway" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Session gateway for Heartwood command and event streams." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/model-policy/pyproject.toml b/packages/model-policy/pyproject.toml index beb016eb..46765df4 100644 --- a/packages/model-policy/pyproject.toml +++ b/packages/model-policy/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-model-policy" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Deny-by-default model-call policy evaluation for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/model-policy/src/heartwood/model_policy/__init__.py b/packages/model-policy/src/heartwood/model_policy/__init__.py index e158f717..66be67a7 100644 --- a/packages/model-policy/src/heartwood/model_policy/__init__.py +++ b/packages/model-policy/src/heartwood/model_policy/__init__.py @@ -23,4 +23,4 @@ "normalize_endpoint", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/notebook/pyproject.toml b/packages/notebook/pyproject.toml index 42c2059c..df8927aa 100644 --- a/packages/notebook/pyproject.toml +++ b/packages/notebook/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-notebook" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Notebook-facing Python API and widget bridge for Heartwood sessions." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/notebook/src/heartwood/notebook/__init__.py b/packages/notebook/src/heartwood/notebook/__init__.py index eb655d02..e221922d 100644 --- a/packages/notebook/src/heartwood/notebook/__init__.py +++ b/packages/notebook/src/heartwood/notebook/__init__.py @@ -39,4 +39,4 @@ "render_widgets", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/schemas/pyproject.toml b/packages/schemas/pyproject.toml index 0c7f8796..c368febc 100644 --- a/packages/schemas/pyproject.toml +++ b/packages/schemas/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-schemas" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Versioned typed schemas for Heartwood policy, audit, detection, and skill metadata records." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/schemas/src/heartwood/schemas/__init__.py b/packages/schemas/src/heartwood/schemas/__init__.py index a73909bd..64dbae9a 100644 --- a/packages/schemas/src/heartwood/schemas/__init__.py +++ b/packages/schemas/src/heartwood/schemas/__init__.py @@ -39,4 +39,4 @@ "schema_names", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/schemas/tests/test_schema_records.py b/packages/schemas/tests/test_schema_records.py index 1dd53b16..61f9fe84 100644 --- a/packages/schemas/tests/test_schema_records.py +++ b/packages/schemas/tests/test_schema_records.py @@ -159,14 +159,14 @@ def test_skill_metadata_accepts_skill_md_aliases() -> None: "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.9", + "heartwood.version": "0.2.0-beta.10", "heartwood.sig": "sigstore:synthetic-bundle", } ) assert metadata.dataset_types == ("omop-cdm", "fhir") assert metadata.platforms == ("generic", "terra") assert metadata.requires_network is False - assert metadata.version == "0.2.0-beta.9" + assert metadata.version == "0.2.0-beta.10" @pytest.mark.parametrize( diff --git a/packages/session/pyproject.toml b/packages/session/pyproject.toml index 1281c343..bafc9296 100644 --- a/packages/session/pyproject.toml +++ b/packages/session/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-session" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Shared session command/event contract for Heartwood interfaces." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/session/src/heartwood/session/__init__.py b/packages/session/src/heartwood/session/__init__.py index 672434c9..1388a0cd 100644 --- a/packages/session/src/heartwood/session/__init__.py +++ b/packages/session/src/heartwood/session/__init__.py @@ -35,4 +35,4 @@ "validate_session_id", ] -__version__ = "0.2.0-beta.9" +__version__ = "0.2.0-beta.10" diff --git a/packages/skills/pyproject.toml b/packages/skills/pyproject.toml index 0c1b5abb..61f8c7f3 100644 --- a/packages/skills/pyproject.toml +++ b/packages/skills/pyproject.toml @@ -6,7 +6,7 @@ [project] name = "heartwood-skills" -version = "0.2.0-beta.9" +version = "0.2.0-beta.10" description = "Local SKILL.md verification and deterministic skill test harnesses for Heartwood." readme = "README.md" requires-python = ">=3.12" diff --git a/packages/webui/package-lock.json b/packages/webui/package-lock.json index d3defb9c..d661913e 100644 --- a/packages/webui/package-lock.json +++ b/packages/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@heartwood/webui", - "version": "0.2.0-beta.9", + "version": "0.2.0-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@heartwood/webui", - "version": "0.2.0-beta.9", + "version": "0.2.0-beta.10", "license": "MIT", "dependencies": { "@stanfordspezi/spezi-web-design-system": "0.20.0", diff --git a/packages/webui/package.json b/packages/webui/package.json index 2df5488d..fd3bde40 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,6 +1,6 @@ { "name": "@heartwood/webui", - "version": "0.2.0-beta.9", + "version": "0.2.0-beta.10", "private": true, "type": "module", "description": "Researcher web UI for Heartwood sessions.", diff --git a/skills/verified/aggregate-export/SKILL.md b/skills/verified/aggregate-export/SKILL.md index 566d2488..a1826c50 100644 --- a/skills/verified/aggregate-export/SKILL.md +++ b/skills/verified/aggregate-export/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.9" + heartwood.version: "0.2.0-beta.10" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/aggregate-export/metadata.json b/skills/verified/aggregate-export/metadata.json index 905702eb..445e978b 100644 --- a/skills/verified/aggregate-export/metadata.json +++ b/skills/verified/aggregate-export/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.9", + "heartwood.version": "0.2.0-beta.10", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/skills/verified/baseline-model/SKILL.md b/skills/verified/baseline-model/SKILL.md index 457dd82f..39b9788d 100644 --- a/skills/verified/baseline-model/SKILL.md +++ b/skills/verified/baseline-model/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.9" + heartwood.version: "0.2.0-beta.10" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/baseline-model/metadata.json b/skills/verified/baseline-model/metadata.json index 905702eb..445e978b 100644 --- a/skills/verified/baseline-model/metadata.json +++ b/skills/verified/baseline-model/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.9", + "heartwood.version": "0.2.0-beta.10", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/skills/verified/omop-cohort-summary/SKILL.md b/skills/verified/omop-cohort-summary/SKILL.md index 94eb58a7..96ed86b9 100644 --- a/skills/verified/omop-cohort-summary/SKILL.md +++ b/skills/verified/omop-cohort-summary/SKILL.md @@ -14,7 +14,7 @@ metadata: heartwood.phi-risk: "none" heartwood.trust-tier: "verified" heartwood.requires-network: "false" - heartwood.version: "0.2.0-beta.9" + heartwood.version: "0.2.0-beta.10" heartwood.sig: "sigstore:synthetic-fixture" --- diff --git a/skills/verified/omop-cohort-summary/metadata.json b/skills/verified/omop-cohort-summary/metadata.json index 905702eb..445e978b 100644 --- a/skills/verified/omop-cohort-summary/metadata.json +++ b/skills/verified/omop-cohort-summary/metadata.json @@ -5,6 +5,6 @@ "heartwood.phi-risk": "none", "heartwood.trust-tier": "verified", "heartwood.requires-network": "false", - "heartwood.version": "0.2.0-beta.9", + "heartwood.version": "0.2.0-beta.10", "heartwood.sig": "sigstore:synthetic-fixture" } diff --git a/uv.lock b/uv.lock index ccb27558..67c9b75c 100644 --- a/uv.lock +++ b/uv.lock @@ -1472,7 +1472,7 @@ wheels = [ [[package]] name = "heartwood-adapters" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/adapters" } dependencies = [ { name = "heartwood-detector" }, @@ -1489,7 +1489,7 @@ requires-dist = [ [[package]] name = "heartwood-audit" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/audit" } dependencies = [ { name = "heartwood-schemas" }, @@ -1500,7 +1500,7 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-cli" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/cli" } dependencies = [ { name = "heartwood-gateway" }, @@ -1521,7 +1521,7 @@ requires-dist = [ [[package]] name = "heartwood-compliance" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/compliance" } dependencies = [ { name = "heartwood-audit" }, @@ -1536,7 +1536,7 @@ requires-dist = [ [[package]] name = "heartwood-core-adapter" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/core-adapter" } dependencies = [ { name = "heartwood-adapters" }, @@ -1557,7 +1557,7 @@ requires-dist = [ [[package]] name = "heartwood-detector" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/detector" } dependencies = [ { name = "heartwood-schemas" }, @@ -1568,12 +1568,12 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-fixtures" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/fixtures" } [[package]] name = "heartwood-gateway" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/gateway" } dependencies = [ { name = "anthropic" }, @@ -1612,7 +1612,7 @@ requires-dist = [ [[package]] name = "heartwood-model-policy" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/model-policy" } dependencies = [ { name = "heartwood-schemas" }, @@ -1623,7 +1623,7 @@ requires-dist = [{ name = "heartwood-schemas", editable = "packages/schemas" }] [[package]] name = "heartwood-notebook" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/notebook" } dependencies = [ { name = "heartwood-gateway" }, @@ -1647,7 +1647,7 @@ provides-extras = ["widgets"] [[package]] name = "heartwood-schemas" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/schemas" } dependencies = [ { name = "pydantic" }, @@ -1658,7 +1658,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12" }] [[package]] name = "heartwood-session" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/session" } dependencies = [ { name = "pydantic" }, @@ -1669,7 +1669,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12" }] [[package]] name = "heartwood-skills" -version = "0.2.0b9" +version = "0.2.0b10" source = { editable = "packages/skills" } dependencies = [ { name = "heartwood-schemas" }, From a0c86b007ff98de94227893992216aea3037e975 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Thu, 23 Jul 2026 01:38:57 -0700 Subject: [PATCH 2/2] Complete Review Coverage --- images/generic/scripts/coding_agent_e2e.sh | 6 ++-- packages/cli/tests/test_cli.py | 10 ++++++ packages/cli/tests/test_launch.py | 41 ++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/images/generic/scripts/coding_agent_e2e.sh b/images/generic/scripts/coding_agent_e2e.sh index 4dc4daea..9f0b53d1 100755 --- a/images/generic/scripts/coding_agent_e2e.sh +++ b/images/generic/scripts/coding_agent_e2e.sh @@ -47,6 +47,8 @@ runtime_port="${HEARTWOOD_LOCAL_RUNTIME_PORT:-8765}" cohort_path="${project}/cohort-summary.json" exact_path="${project}/heartwood-exact-output.txt" rejected_path="${project}/heartwood-rejected-output.txt" +exact_name="$(basename -- "${exact_path}")" +rejected_name="$(basename -- "${rejected_path}")" events_path="${workspace}/${session_id}/events.jsonl" audit_path="${state_root}/audit-export.jsonl" @@ -141,7 +143,7 @@ run_heartwood models connect heartwood heartwood-managed-runtime | tee -a "${tra run_heartwood models validate heartwood | tee -a "${transcript}" run_heartwood actions set ask-every-time | tee -a "${transcript}" run_heartwood --session-id "${session_id}" \ - --prompt "Call the terminal tool to execute this exact command: ${heartwood_python} ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && printf 'heartwood-agent-exact-ok\\n' > heartwood-exact-output.txt && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ + --prompt "Call the terminal tool to execute this exact command: ${heartwood_python} ${runtime_root}/skills/verified/omop-cohort-summary/scripts/run.py --data-root input --target-condition-concept-id 201826 --minimum-age 18 --aggregate-count-floor 20 --output cohort-summary.json && printf 'heartwood-agent-exact-ok\\n' > ${exact_name} && cat cohort-summary.json. Do not describe the command as text and do not call another tool after it completes. Wait for the terminal result, then report the aggregate cohort result." \ | tee -a "${transcript}" for _ in 1 2 3 4; do @@ -153,7 +155,7 @@ for _ in 1 2 3 4; do done run_heartwood --session-id "${session_id}" \ - --prompt "Call the terminal tool once to execute exactly: printf 'this-action-must-remain-rejected\\n' > heartwood-rejected-output.txt. Do not call another tool." \ + --prompt "Call the terminal tool once to execute exactly: printf 'this-action-must-remain-rejected\\n' > ${rejected_name}. Do not call another tool." \ | tee -a "${transcript}" pending_id="$(pending_action_id)" diff --git a/packages/cli/tests/test_cli.py b/packages/cli/tests/test_cli.py index b598d467..e1803a19 100644 --- a/packages/cli/tests/test_cli.py +++ b/packages/cli/tests/test_cli.py @@ -18,6 +18,7 @@ from heartwood.adapters.platform import GenericPlatformAdapter from heartwood.cli import ( + _MODEL_DOWNLOAD_ACTIVITY, __version__, _consume_prompt, _float_payload, @@ -135,6 +136,15 @@ def test_version_is_available(capsys: pytest.CaptureFixture[str]) -> None: assert f"heartwood {__version__}" in capsys.readouterr().out +def test_model_preparation_progress_explains_long_running_work() -> None: + assert _MODEL_DOWNLOAD_ACTIVITY.label == "Preparing and verifying the model" + assert _MODEL_DOWNLOAD_ACTIVITY.waiting_label == "Still preparing and verifying the model" + assert _MODEL_DOWNLOAD_ACTIVITY.guidance == ( + "Large downloads and full verification of existing model files can take several minutes. " + "Keep this process running." + ) + + def test_line_mode_reports_elapsed_progress_for_a_slow_turn( capsys: pytest.CaptureFixture[str], ) -> None: diff --git a/packages/cli/tests/test_launch.py b/packages/cli/tests/test_launch.py index b183ba04..c0eb4079 100644 --- a/packages/cli/tests/test_launch.py +++ b/packages/cli/tests/test_launch.py @@ -499,6 +499,47 @@ def runner(_command: Sequence[str]) -> int: assert not plan.model_root.exists() +def test_real_launch_plan_cannot_download_a_recommendation_before_setup( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr( + "heartwood.gateway._gpu_environment.discover_visible_gpus", + lambda _env: tuple( + GpuDevice( + index=index, + name="Tesla T4", + total_memory_bytes=16 * 1024**3, + free_memory_bytes=15 * 1024**3, + driver_version="570.86.15", + compute_capability=(7, 5), + ) + for index in range(2) + ), + ) + options = _options( + tmp_path, + selected=False, + gpus=None, + yes_download=True, + yes_request_allocation=True, + ) + runner_called = False + + def runner(_command: Sequence[str]) -> int: + nonlocal runner_called + runner_called = True + return 0 + + assert run_launch(options, env={"HEARTWOOD_PLATFORM": "terra"}, run_fn=runner) == 64 + output = capsys.readouterr().out + assert "Model status: recommendation only" in output + assert "Run `heartwood` to choose Run with Heartwood" in output + assert not runner_called + assert not any(options.project.models_dir.iterdir()) + + def test_launch_rejects_short_context_before_starting_runtime( tmp_path: Path, capsys: pytest.CaptureFixture[str],