Skip to content

feat: deployment jobs argument, auto opTokens, and fail-loud multi-op guard - #378

Closed
kingpanther13 wants to merge 95 commits into
mainfrom
issue-376-deployment
Closed

feat: deployment jobs argument, auto opTokens, and fail-loud multi-op guard#378
kingpanther13 wants to merge 95 commits into
mainfrom
issue-376-deployment

Conversation

@kingpanther13

@kingpanther13 kingpanther13 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds the deployment argument to hub_set_rule / hub_set_native_app (no new tools): durable multi-app migration jobs that checkpoint on-hub after every op and survive disconnects — op: create|resume|commit|cancel|delete|status.
  • Every untokened write is now auto-recorded with an auto-… opToken (buffered result, hub_get_info(includeRecentOps=true) journal). Marker-tracked long-running writes also get canonical-argument duplicate_in_flight refusal and participate in maxConcurrentWrites backpressure (default 2; 0 disables). A dropped response can be recovered from its retained journal record without the client having pre-arranged a token.
  • Multi-op EDIT calls now refuse fail-loud (steering to patches:[…]) instead of silently running only the first op. Part of Feature request: durable, resumable batch deployments with validation and atomic cutover for native automations #376.

Type of change

  • feat — new feature or capability

Changes

  • Deployment jobs (libraries/mcp-deploy-jobs-lib.groovy, new): staged clone/import/edit ops with alias resolution, on-hub worker slices, a health-check validation gate before ready_for_commit, commit cutover, cancel rollback (deletes only created apps), and delete for finished job records. Same engine from both tools; self-contained call; status is a pure read. Cancel no longer stalls on a deleted app (existence is read directly instead of through a fetch that throws for a missing app, which conflated "gone" with "unreadable"); a slice's lease is released by the same save that publishes its terminal phase; an unusable created id fails its op rather than passing a bad alias downstream.
  • opToken machinery (hubitat-mcp-server.groovy): auto-token on every untokened write; opToken declared on every tokened write tool's schema; canonical-args fingerprint powering duplicate_in_flight; too_many_writes_in_flight backpressure (self-admin settable, 0 disables); token journal on hub_get_info. A dead write's record is identified exactly (app-instance start stamp) rather than waited out, and the running-record window is 180s. A bounded newest-three terminal-result backstop preserves prompt token-only replay if the scheduled whole-map sweep wins the narrow final-write race against a just-completed record.
  • Write-path cost: the running marker is written only for tools that can outlive their transport, the record map is read once per call instead of per check, and exceeding 20 records batch-evicts the oldest terminal entries down to 10 — the auto-token path taxes every write, so its per-call cost is load-bearing.
  • Op-result hygiene: orphaned result files are reaped (never one young enough to belong to an in-flight write), the sweep re-arms itself, markerless records keep their tool name so the reap can attribute them, and immediate recovery survives the sweep/completion race without restoring the ~0.9s per-write verification read.
  • Flat-catalog budget: [[FLAT_TRIM]] defers long optional description tails while preserving basic-call constraints in flat mode and full detail in gateway mode/the served guide. The transform recurses through nested schemas and no marker tokens leak into the catalog. Current CI measures 116,852 B in the 122,000 B tripwire configuration and 118,424 B at its widest, below the hub's 124,000 B cap; opToken constraints remain flat-visible.
  • Fail-loud batch guard (mcp-native-rules-lib.groovy): 2+ operative families in one EDIT call throw before any write (the documented addTriggers+addActions bulk pair stays allowed); settingsApplied deduped.
  • hub_get_rule_health: authoritative stopped boolean (config-page label/status, HTML-stripped), per-rule paused/disabled state, and live eventSubscriptionCount/scheduledJobCount. A rule literally named … (Stopped) is no longer false-flagged, and the runtime suffix is stripped from returned labels so name lookup still resolves.
  • hub_list_files: filter substring param, op-result buffers hidden by default unless includeOpResults=true, and response-too-large guidance that names the filter.
  • Error-text and description fixes: nonexistent-rule 404 leads with the fact; canonical "Certain Time (and optional date)" picker name; RM inverted verb/boolean storage quirk documented; recentOpsLimit is validated before identifyHub blinks the LED; engine text pointers updated to the argument surface.
  • Guides: deployment_jobs section (served + TOOL_GUIDE.md); slow_ops rewritten auto-record-first.
  • BAT suites: zero-context runner contract documented; T311/T432/T433/T661 corrected; T465/T466 deployment scenarios added.

Release Notes

  • Staged multi-app migrations: hub_set_rule/hub_set_native_app accept a deployment argument that runs clone/edit/cutover jobs on the hub with checkpoints — a dropped connection does not lose completed progress
  • Timed-out or dropped write responses can be recovered from the recent-operation journal by replaying the retained automatic operation token — no need to arrange a token before the call
  • Duplicate marker-tracked long-running writes and writes exceeding the configured in-flight limit are refused instead of double-running (maxConcurrentWrites advanced setting, default 2)
  • Batch edits that would have silently dropped operations now refuse with guidance to use patches
  • hub_get_rule_health reports paused, disabled, and stopped state plus live subscription/schedule counts; hub_list_files supports name filtering and hides operation-result buffers by default
  • Operation-result files left behind by interrupted writes are cleaned up automatically instead of accumulating

Testing

  • Spock diff: ToolDeploymentJobsSpec (new, 1,435 lines), OpTokenReplaySpec (+513/-49), RelayBudgetSpec (+343), ToolRmNativeCrudSpec (+420/-1), HubInfoFieldContractSpec (+101), plus ToolListRmRulesSpec, ToolManageFilesSpec, and ToolUpdateMcpSettingsSpec. The Groovy 2.5 lane keeps its own harness copy, so the runIn recorder is carried in both.
  • e2e: test_deployment_job_lifecycle and test_deployment_manifest_validated_up_front cover the deployment surface. Committing writes are never transport-replayed after a 504/network/decode failure; the runner surfaces a typed lost-response error and targeted helpers recover through exact token-only replay, state verification, or unique-name/job adoption. The deployment lifecycle polls its create/commit token, adopts a uniquely named job if the create result is still unavailable, and reconciles/resumes a stale lost-create lease. tools/list gets a longer read retry budget, and the serial suite pins maxConcurrentWrites: 0 outside the dedicated live-cap test.
  • Latest full run on head 27b6ca6e: 219/219 e2e tests passed in 4,096.3s, including deployment recovery through multiple relay 504s, the genuinely overlapping too_many_writes_in_flight refusal, and untokened-write journal/replay coverage. Official MCP Python SDK conformance passed 8/8 scenarios; all fast CI lanes and CodeRabbit are green.
  • Manual live chaos verification with E2E_CHAOS_504: a write commits, its response is replaced with the relay-504 failure, the client replays the buffered result from its token, and the rule gains exactly one action — verified through both a leaf call and a hard-coded gateway envelope.
  • Zero-context A/B BAT comparison (19 scenarios): failures 1→0, client-token adoption 7→0 by design, costs ≈flat.

Checklist

  • Unit tests added for any new MCP tools, regressions, or bug fixes (required — see docs/testing.md for the harness + recipes)
  • e2e tests added for new tools and/or regression tests added for any bug fix (see tests/e2e_test.py)
  • Sandbox lint passes: python tests/sandbox_lint.py
  • ./gradlew test passes locally (or CI confirms)
  • Live-hub BAT tests updated if tool behaviour changed (see tests/BAT-v2.md)
  • Documentation updated if user-facing behaviour or tool surface changed
  • New/renamed MCP tools follow AGENTS.md Tool Design Rules (naming, annotations, schema)

Summary by CodeRabbit

  • New Features

    • Added staged, resumable multi-application deployment jobs with validation, progress tracking, rollback, cancellation, and health checks.
    • Added automatic operation tokens across write actions for safer retries and replay after interrupted responses.
    • Added recent-operation history and configurable concurrent-write limits.
    • File listings now support filtering and hide operation-result files by default.
    • Rule status and health reporting now identify stopped rules and include runtime details.
  • Documentation

    • Expanded guidance for deployments, recovery, retries, lifecycle management, and rule status behavior.
    • Clarified boolean field representations and deployment-job usage.

…ecorded untokened writes

Ports the issue-376 engine as a deployment ARGUMENT (no new tools): both
native-app write tools route args.deployment to one shared job engine --
op create/resume/commit/cancel (confirm-gated) + op status as the pure
read mode. Jobs checkpoint to hub storage after every op and advance
on-hub via the scheduler with no client attached; staging auto-validates
before ready_for_commit; cancel deletes only job-created apps.

Also closes the token-less recovery hole: every untokened WRITE now gets
a server-assigned auto- opToken (recorded + result buffered exactly like
a client token, token returned on the response), and
hub_get_info(includeRecentOps=true) lists the op journal so a client
that lost a response can find what ran and replay it token-only instead
of blind-retrying. slow_ops guide states the token is the assistant's to
invent and record -- never the user's.
…first token contract

Untokened writes now get full duplicate protection without any client
setup: an identical call (canonical-args fingerprint) arriving while its
twin's record is still running is refused with the in-flight token to
poll, and a maxConcurrentWrites cap (advanced setting, default 2,
1=serial, 0=off, also settable via hub_update_mcp_settings) refuses
writes past the concurrency limit, naming the in-flight ops. Both checks
age-cap at 10 minutes so a crash-stranded record cannot wedge writes.
Client-tokened calls keep their own dedup and skip the fingerprint check
(an explicit new token = intentional re-run).

slow_ops guide and the opToken descriptions now present the auto-record
as the primary mechanism (client tokens = optional verbatim-retry extra)
and state record lifetimes. Every write tool's outputSchema declares the
returned auto token. Dispatch specs + a live-hub e2e (auto token ->
journal -> token-only replay) cover the new surface.

Implemented by codex-cli; reviewed, line-ending-normalized, and verified.
…ror)

A bare assignment cannot continue onto a leading-&& line -- the hub's
parser rejected the class at line 1640 (live deploy caught it; sandbox
lint does not parse). Wrapped in parens like every other multi-line
boolean in the file.
…AT findings

Two wire-verified bugs from the zero-context BAT runs:
- hub_set_rule EDIT with multiple operation families silently ran only
  the first while reporting clean success. Now rejected fail-loud BEFORE
  any write, naming the ops and steering to patches; the documented
  addTriggers+addActions bulk pair stays allowed. settingsApplied
  deduped.
- A completed op's journal record could stick at 'running' when the
  prune's whole-map rewrite clobbered the per-entry completion write.
  _opTokenComplete/_opTokenRelease now re-read after writing and re-write
  the terminal state once if reverted.

Plus the queued findings: hub_get_rule_health now returns live
eventSubscriptionCount/scheduledJobCount (the readback T311's agents
proved unreachable); hub_list_rules reports the stopped sub-state and
strips the '(Stopped)' suffix; nonexistent-rule edits lead with 'No
rule/app with id N exists'; addTrigger names the canonical 'Certain Time
(and optional date)' picker string; pvTF/pR inverted-storage quirk
documented; hub_list_files gains the name filter its callers expected;
TOOL_GUIDE slow-ops synced with the served guide; BAT suites get the
zero-context runner contract, T311/T432/T433 rewrites matching the real
tool surface, and T661's fail-loud pass-path. Spock coverage for the
guard, the allowed bulk pair, and the completion re-write.

Implemented by codex-cli (gpt-5.6-sol); reviewed and verified.
…e it)

Live verification caught the stopped-status fix not working through
hub_list_rules: the cheap RMUtils/appsList sources never carry the
'(Stopped)' decoration on this firmware -- it only surfaces on per-app
pages. hub_get_rule_health now derives an authoritative
boolean from the already-fetched config label (and strips the raw HTML
span + decoration from the returned label, which previously leaked).
List/docs/BAT wording corrected: list-level 'stopped' is best-effort,
health is authoritative; a stopped rule's statusJson omits its
subscription list, so eventSubscriptionCount reads null (not 0) while
stopped -- documented and T311 reworded accordingly.
…ototype pointers in engine text

The deployment argument had no way to remove a terminal job record (they
persisted until the 8-job cap forced a lazy evict). op=delete removes a
completed/cancelled record only -- apps and backups untouched, rollback
handles surfaced one last time. Engine error/note text still pointed at
the prototype-era hub_get_deployment tool and operation= key; all sites
now steer to the real surface (deployment={op:...}).

Adds the missing test coverage for the whole argument: ToolDeploymentJobsSpec
(routing, status read, create fail-before-persist, delete gating), a
deployment lifecycle e2e scenario (native_apps group + e2e_scope map entry),
and BAT T465/T466.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3fc5acd2-5af4-475c-a918-329d366dc31f

📥 Commits

Reviewing files that changed from the base of the PR and between 2f61af7 and 27b6ca6.

📒 Files selected for processing (2)
  • hubitat-mcp-server.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
📜 Recent review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: e2e (run)
  • GitHub Check: test (strict, flat)
  • GitHub Check: test (normal, gateway)
  • GitHub Check: test (normal, flat)
  • GitHub Check: test (strict, gateway)
  • GitHub Check: groovy2x-spock
🧰 Additional context used
📓 Path-based instructions (4)
**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.groovy: Use atomicState for thread-safe persistence, state for UI/counters, and compare device IDs as strings with toString().
Add comments only when explaining non-obvious rationale; avoid multi-paragraph docblocks and references to the current PR, issue, or caller.
Do not ship deprecation aliases when renaming non-conforming MCP tools; rename clients and server references in lockstep.
Use unambiguous parameter names such as device_id and user_id, naming complex parameters after their semantic value.
Merge opposite state mutations into one set__ tool, merge filter/projection variants with optional parameters, and avoid consolidation when error modes, safety gates, or payload shapes differ fundamentally.
Every new MCP tool must explicitly classify readOnlyHint, idempotentHint, and openWorldHint; emit destructiveHint for writes and omit it for reads.
Every leaf tool and gateway must have display metadata with a unique Title-Case title and a one-sentence summary of at most 140 characters ending in a period.
Keep annotation hints advisory; enforce read/write permissions through the universal master gates and destructive operations through confirmation and backup checks.
Tool descriptions must begin with a concise summary, include usage and safety guidance, expose implicit constraints, and keep examples in parameter descriptions or the served guide rather than stuffing them into the body.
Use inputSchema objects, enums for fixed values, omit required when no parameters are mandatory, and declare an outputSchema on every new tool definition with nullable fields modeled accurately.
When output schemas are published, return structuredContent for every successful advertised tool result and emit the wire-form schema with recursive required fields removed.
Validation failures must throw IllegalArgumentException before any side effect; runtime operation failures must return a structured [success:false, error, note] result and use isError:true in the MCP...

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
src/test/groovy/**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

Every new MCP tool requires both a direct-call unit test and a dispatch-envelope integration test.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
🧠 Learnings (4)
📓 Common learnings
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 378
File: libraries/mcp-deploy-jobs-lib.groovy:555-571
Timestamp: 2026-08-09T12:53:41.770Z
Learning: In `libraries/mcp-deploy-jobs-lib.groovy`, `atomicState.updateMapValue` is used for per-job deployment checkpoints, but its null-value deletion semantics are undocumented. `_deployOpDelete` therefore rebuilds `atomicState.deployJobs` only for terminal deployment jobs, consistent with the existing terminal-job pruning pattern. Lease guards must prevent cancellation or deletion while an active deployment worker holds `sliceLeaseUntil`, because the worker saves a whole-job snapshot.
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: :0-0
Timestamp: 2026-08-10T17:02:34.806Z
Learning: In `hubitat-mcp-server.groovy`, token-only operation-result recovery is handled by `_isOpTokenPollShape` in `handleToolsCall` before gateway required-parameter enforcement. `requiredParamsByTool()` is derived from each tool definition’s `inputSchema.required`; preserve these required declarations so the catalog accurately advertises required tool arguments and poll-shape detection remains aligned with runtime validation.
📚 Learning: 2026-08-08T03:29:24.112Z
Learnt from: CR
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-08-08T03:29:24.112Z
Learning: Applies to tests/e2e_test.py : Add or update e2e coverage for observable tool, dispatch, gateway, transport/protocol, and bug-fix behavior; keep tool names and scenarios synchronized with the server.

Applied to files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
📚 Learning: 2026-08-10T17:02:34.806Z
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: :0-0
Timestamp: 2026-08-10T17:02:34.806Z
Learning: In `hubitat-mcp-server.groovy`, token-only operation-result recovery is handled by `_isOpTokenPollShape` in `handleToolsCall` before gateway required-parameter enforcement. `requiredParamsByTool()` is derived from each tool definition’s `inputSchema.required`; preserve these required declarations so the catalog accurately advertises required tool arguments and poll-shape detection remains aligned with runtime validation.

Applied to files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
📚 Learning: 2026-08-09T12:53:41.770Z
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 378
File: libraries/mcp-deploy-jobs-lib.groovy:555-571
Timestamp: 2026-08-09T12:53:41.770Z
Learning: In `libraries/mcp-deploy-jobs-lib.groovy`, `atomicState.updateMapValue` is used for per-job deployment checkpoints, but its null-value deletion semantics are undocumented. `_deployOpDelete` therefore rebuilds `atomicState.deployJobs` only for terminal deployment jobs, consistent with the existing terminal-job pruning pattern. Lease guards must prevent cancellation or deletion while an active deployment worker holds `sliceLeaseUntil`, because the worker saves a whole-job snapshot.

Applied to files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
🔇 Additional comments (1)
src/test/groovy/server/OpTokenReplaySpec.groovy (1)

604-634: LGTM!


📝 Walkthrough

Walkthrough

The PR adds durable multi-application deployment jobs, automatic operation-token replay, concurrent-write limits, stopped-rule health reporting, file filtering, expanded tool schemas, and automated coverage.

Changes

Server behavior and deployment

Layer / File(s) Summary
Deployment jobs and routing
libraries/mcp-deploy-jobs-lib.groovy, hubitat-mcp-server.groovy, libraries/mcp-native-rules-lib.groovy
Adds checkpointed creation, resume, commit, cancellation, rollback, deletion, status reporting, leases, health gates, aliases, and native-rule routing.
Operation replay and write limits
hubitat-mcp-server.groovy, libraries/mcp-system-lib.groovy, libraries/mcp-self-admin-lib.groovy
Adds automatic tokens, replay records, result buffering, cleanup, recent-operation reporting, and configurable concurrent-write limits.
Rule and file handling
libraries/mcp-native-rules-lib.groovy, libraries/mcp-files-lib.groovy
Adds stopped-rule status and health counts, validates incompatible edits, deduplicates settings, and filters operation-result files before pagination.

Tool contracts and validation

Layer / File(s) Summary
Tool contracts and documentation
libraries/mcp-*-lib.groovy, TOOL_GUIDE.md, docs/rm_action_subtype_schemas.md
Updates operation-token schemas, deployment guidance, recent-operation fields, stopped-rule documentation, and flat-trim metadata.
Packaging and test support
tools/build-bundle.py, .github/scripts/*, src/test/groovy/support/*, ci/groovy2x-spock/scaffold/support/HarnessSpec.groovy
Packages the deployment library, updates E2E scope and probes, validates backup setup, and records scheduled worker calls.
Validation coverage
src/test/groovy/*, tests/*
Adds deployment, replay, concurrency, health, filtering, transport-recovery, schema, BAT, and end-to-end tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested labels: e2e:full

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The description includes all required sections, selects one change type, documents user-facing changes, testing, release notes, and checklist completion.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-376-deployment

Comment @coderabbitai help to get the list of available commands.

…to the branch's new behavior

The reachability guard caught a real gap: the deployment_jobs guide section
was served but missing from hub_get_tool_guide's section enum. The remaining
failures were stale pins on intended changes: dispatch tests now see the
auto-token op-result buffer upload (filtered at the capture stubs), the
hub_list_rules status enum gained 'stopped', hub_set_native_app's schema
gained 'deployment', and the nonexistent-rule backup error leads with the
404 fact instead of backup mechanics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (13)
src/test/groovy/server/ToolManageFilesSpec.groovy (1)

57-74: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Extend hub_list_files filter coverage.

  • Pass filter through mcpDriver.callTool in both gateway modes.
  • Add a case-insensitive filter assertion for the HTML fallback.
  • Add a cursor-pagination case that proves filtering occurs before pagination.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/groovy/server/ToolManageFilesSpec.groovy` around lines 57 - 74,
Extend the hub_list_files tests around the existing “hub_list_files filters file
names by case-insensitive substring” case: invoke the tool through
mcpDriver.callTool with filter in both gateway modes, add an HTML-fallback
assertion for case-insensitive filtering, and add cursor-pagination coverage
proving the filter is applied before pagination.

Source: Coding guidelines

libraries/mcp-deploy-jobs-lib.groovy (3)

407-407: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make jobId unique against same-millisecond creates.

"dj-${now()}" collides if two create calls land in the same millisecond. _deploySaveJob then overwrites the first job record through updateMapValue, and the first job's createdAppIds are lost, so its apps can no longer be rolled back by op='cancel'. Append a short random or counter suffix.

♻️ Proposed change
-    def jobId = "dj-${now()}".toString()
+    def jobId = "dj-${now()}-${(int) (Math.random() * 10000)}".toString()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-deploy-jobs-lib.groovy` at line 407, Update the jobId
construction in _deploySaveJob to append a short uniqueness suffix to the
current timestamp, using an existing random or counter utility where available.
Preserve the “dj-” prefix and timestamp while ensuring same-millisecond create
calls produce distinct IDs.

290-300: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Write the mutated statusList back to the job before saving on the gated path.

The addActions gate mutates statusList[idx] at line 294 and calls _deploySaveJob(job) at line 298, but it never assigns job.opStatus/job.commitStatus the way line 338 does. The code works today only because ((staging ? job.opStatus : job.commitStatus) ?: []) as List returns the same list instance. If job.opStatus is ever absent or is deserialized into a non-List collection, the elvis branch builds a detached list and the failed/interrupted marks are lost on save. The adopt branch at lines 279-286 has the same gap.

Assign the list back before each _deploySaveJob call, as line 338 does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-deploy-jobs-lib.groovy` around lines 290 - 300, Assign the
mutated statusList back to the appropriate job.opStatus or job.commitStatus
field before calling _deploySaveJob(job) in the addActions gated path. Apply the
same assignment in the adopt branch, preserving the staging-based field
selection used by the existing status-list logic.

356-366: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound the worker re-arm when a slice throws repeatedly.

If _deployRunSlice throws before it can mark the job failed, the catch at line 358 logs, the finally clears the lease, and line 366 re-arms the worker because the job is still in an active phase. The next run repeats the same throw. The job then re-schedules every 15 seconds forever and no phase transition ends it.

Count consecutive slice exceptions on the job record. When the count passes a small limit, set phase = "failed" with the last error so the operator sees it and the re-arm stops.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-deploy-jobs-lib.groovy` around lines 356 - 366, Update the
worker exception handling around _deployRunSlice to track consecutive slice
exceptions on the job record, incrementing the count and retaining the latest
error. When the count exceeds a small bounded retry limit, set the job phase to
"failed" with the last error so the existing stillActive check stops re-arming;
reset the consecutive-exception count after a successful slice.
libraries/mcp-native-rules-lib.groovy (2)

7757-7758: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider unique(false) instead of a manual dedup loop.

uniqueApplied builds a deduplicated list with a manual contains check inside each. List.unique(false) does the same in one call, preserving insertion order without mutating applied.

♻️ Proposed simplification
-    def uniqueApplied = []
-    applied.each { key -> if (!uniqueApplied.contains(key)) uniqueApplied << key }
+    def uniqueApplied = applied.unique(false)

Also applies to: 7768-7768

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-native-rules-lib.groovy` around lines 7757 - 7758, Replace the
manual deduplication loops for uniqueApplied, including the corresponding logic
at the second occurrence, with List.unique(false) on applied. Preserve insertion
order and avoid mutating the original applied list.

152-161: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

deployment is undocumented in the flat self-gateway tool.

toolSetRule now dispatches args.deployment before the operation envelope is even inspected (line 9426-9431), so a flat-mode caller (useGateways=false) can technically send deployment. But _setRuleFlatTool() — the description and inputSchema served in flat mode — never mentions deployment, and _setRuleOperations() does not include it as a selectable operation. An AI client relying on the flat tool's self-description has no way to discover this capability exists.

Add a brief mention of deployment to _setRuleFlatTool()'s description (and consider adding the property to its inputSchema) so flat-mode callers can discover the feature the same way gateway-mode callers can from this fat schema.

Also applies to: 283-293

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-native-rules-lib.groovy` around lines 152 - 161, Update
_setRuleFlatTool() to document the deployment capability in its flat-mode
description and inputSchema, matching the existing deployment definition and
behavior exposed by the gateway schema. Ensure flat-mode callers can discover
and submit deployment without changing dispatch or operation handling.
tests/BAT-v2.md (1)

4627-4627: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a dedicated scenario for the mixed bare-argument rejection.

T661 only asks for several changes in one batch. An agent can use patches directly and pass without exercising the new fail-loud guard. Keep T661 focused on in_progress resume behavior. Add a separate goal-framed BAT case that requires the rejection, followed by a successful patches retry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/BAT-v2.md` at line 4627, Add a separate goal-framed BAT scenario for
mixed bare-argument batches that verifies the call refuses fail-loud without
partial application, then retries the same changes through patches successfully.
Remove this rejection requirement from T661 so it remains focused solely on
in_progress resume behavior.
hubitat-mcp-server.groovy (3)

1573-1591: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Auto-token generation uses a 16-bit random suffix.

Two writes started in the same millisecond can collide with probability 1/65536. A collision makes the second _opTokenMark overwrite the first record, so the first operation's poll replays the wrong buffered result. The concurrency cap makes this rare, but a wider suffix removes the case at no cost.

♻️ Wider random suffix
-                opToken = "auto-" + Long.toString(now(), 16) + "-" + Integer.toString(new Random().nextInt(0xFFFF), 16).padLeft(4, '0')
+                opToken = "auto-" + Long.toString(now(), 16) + "-" + Long.toString(Math.abs(new Random().nextLong()), 16).padLeft(12, '0')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hubitat-mcp-server.groovy` around lines 1573 - 1591, Update auto-token
generation in the !opTokenActive block to use a wider random suffix than the
current 16-bit nextInt(0xFFFF) value, while preserving the existing timestamp
prefix, formatting, and token assignment behavior.

2137-2180: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the fingerprint is a hash plus length.

_canonicalOpArgs, _findIdenticalRunningOp, and _recentRunningWriteOps carry no comments, while every neighbouring token helper explains its rationale. The non-obvious part is the fingerprint design: the record stores fpHash + fpLen instead of the canonical JSON, and duplicate detection therefore accepts a small false-positive risk in exchange for a bounded atomicState record. Add one short comment stating that trade-off and the 10-minute recency window.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hubitat-mcp-server.groovy` around lines 2137 - 2180, Add a brief comment near
the fingerprint handling in _canonicalOpArgs or _findIdenticalRunningOp
documenting that fpHash plus fpLen avoids storing canonical JSON, keeps
atomicState records bounded, and permits a small false-positive risk; also
mention that duplicate detection is limited to the 10-minute recency window used
by _findIdenticalRunningOp and _recentRunningWriteOps.

1639-1672: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a retry regression test for too_many_writes_in_flight.

RelayBudgetSpec.groovy already covers both refusal statuses and no orphan records. Add a test that re-issues the same client-tokened call after the blocking record expires and confirms execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hubitat-mcp-server.groovy` around lines 1639 - 1672, Add a regression test in
RelayBudgetSpec.groovy for too_many_writes_in_flight that first verifies
refusal, advances or waits until the blocking in-flight record expires, then
re-issues the same client-tokened call and asserts it executes successfully.
Reuse the existing setup and no-orphan-record assertions, ensuring the expired
blocker no longer prevents retry.
src/test/groovy/server/RelayBudgetSpec.groovy (1)

195-220: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case pinning that the concurrency cap still applies to a client-tokened write.

This spec pins that a client token bypasses the fingerprint duplicate check. The implementation deliberately applies the two gates asymmetrically: _findIdenticalRunningOp runs only when opTokenAuto is true, while the maxConcurrentWrites gate runs for every write leaf (hubitat-mcp-server.groovy Lines 1641 and 1657). Without a test, a later refactor could move the cap inside the opTokenAuto branch and let client-tokened writes escape it unnoticed.

💚 Suggested additional case
def "a client-tokened write is still subject to maxConcurrentWrites"() {
    given:
    settingsMap.enableWrite = true
    settingsMap.maxConcurrentWrites = 1
    atomicStateMap.opTokens = [
        'auto-room-create': [
            state: 'running', tool: 'hub_create_room', startedAt: FIXED_NOW - 1000L
        ]
    ]
    def ran = 0
    script.metaClass.toolRenameRoom = { Map args -> ran++; [success: true] }

    when:
    def response = mcpDriver.callTool('hub_update_room', [
        room: 'Den', newName: 'Study', confirm: true, opToken: 'client-retry-2'
    ])

    then:
    ran == 0
    mcpDriver.parseInner(response).status == 'too_many_writes_in_flight'
    atomicStateMap.opTokens['client-retry-2'] == null
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/groovy/server/RelayBudgetSpec.groovy` around lines 195 - 220, Add a
test alongside the existing client-tokened write spec that sets
maxConcurrentWrites to 1, seeds one running operation, and invokes a different
write with a client opToken. Assert the tool does not run, the parsed response
reports too_many_writes_in_flight, and no client token record is created,
preserving the cap independently of fingerprint duplicate handling.
tests/e2e_test.py (2)

3567-3582: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap hub_call_rule stop/start with the same limiter-recovery pattern as the adjacent writes.

The pause (hub_set_rule_paused) and disabled (hub_set_app_disabled) writes surrounding this block route through _status_write, which bounces the app via the watchdog and retries once on an "excessive hub load" limiter trip. The new hub_call_rule(action=stop) / hub_call_rule(action=start) calls are issued directly and have no such recovery, even though they write to the same rule under the same load conditions this test already documents as limiter-prone.

Consider routing these two calls through _status_write (or an equivalent bounce-and-retry wrapper) so a limiter trip degrades to a retry instead of a hard test failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e_test.py` around lines 3567 - 3582, Update the stop/start calls in
the rule lifecycle test to use the existing _status_write limiter-recovery
pattern, or an equivalent watchdog bounce-and-single-retry wrapper. Preserve the
current hub_call_rule actions, success assertions, and status polling while
ensuring excessive hub load is retried instead of failing immediately.

4187-4191: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Remove the deployment job record during teardown.

If job_id exists, cancel the job first when it is unfinished, then call deployment={"op": "delete", "jobId": job_id}. op="cancel" removes created apps but leaves the job record. Catch cleanup errors so teardown does not mask the test failure or leave duplicate job_name records for recovery lookup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e_test.py` around lines 4187 - 4191, Update the teardown finally
block around _delete_native to also clean up job_id: if present and unfinished,
cancel it first, then delete the deployment with deployment={"op": "delete",
"jobId": job_id}. Catch cleanup errors so teardown preserves the original test
failure, and ensure the job record is removed to avoid duplicate job_name
recovery records.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hubitat-mcp-server.groovy`:
- Around line 8226-8275: The deployment_jobs guide content exists, but its
section name is missing from the hub_get_tool_guide section enum. Update the
enum in the hub_get_tool_guide schema to include deployment_jobs, matching the
existing section returned by getToolGuideSections() and documented in
TOOL_GUIDE.md.

In `@libraries/mcp-app-cloner-lib.groovy`:
- Line 897: Add the optional client-supplied opToken property to the input
schemas for hub_export_native_app in libraries/mcp-app-cloner-lib.groovy at
lines 897-897, hub_delete_bundle in libraries/mcp-bundles-lib.groovy at lines
448-448, and hub_export_bundle in libraries/mcp-bundles-lib.groovy at lines
475-475, matching the existing operation-token type and description conventions
used by their output schemas.

In `@libraries/mcp-code-management-lib.groovy`:
- Line 2998: Update the hub_delete_item input schema alongside its existing
result opToken field to declare an optional opToken input property, including
the same transport-recovery and token-only replay guidance used by the other
write tools. Keep the existing output schema unchanged.

In `@libraries/mcp-custom-rules-lib.groovy`:
- Line 843: Replace the stray “auto-” wording in the opToken schema descriptions
with “Server-assigned automatic operation token” for hub_create_custom_rule,
hub_update_custom_rule, hub_delete_custom_rule, hub_export_custom_rule,
hub_import_custom_rule, and hub_clone_custom_rule in
libraries/mcp-custom-rules-lib.groovy at lines 843, 877, 900, 947, 987, and
1017; hub_create_dashboard, hub_update_dashboard, hub_delete_dashboard, and
hub_clone_dashboard in libraries/mcp-dashboards-lib.groovy at lines 1081, 1111,
1135, and 1155; and hub_delete_debug_logs and hub_set_log_level in
libraries/mcp-debug-logging-lib.groovy at lines 518 and 538. Preserve the
existing replay guidance.

In `@libraries/mcp-deploy-jobs-lib.groovy`:
- Around line 520-530: In libraries/mcp-deploy-jobs-lib.groovy:520-530, update
_deployOpCancel to apply the same sliceLeaseUntil guard used by _deployOpResume
before deleting targets. In libraries/mcp-deploy-jobs-lib.groovy:661-671, update
_deployOpDelete to apply that guard before removing the record and delete only
the targeted job entry rather than rewriting the entire deployJobs map,
preserving concurrent _deploySaveJob updates for other jobs.
- Line 1: Remove the “(issue `#376`)” suffix from the library description in the
McpDeployJobsLib declaration, preserving the rest of the user-visible
description unchanged.
- Around line 43-49: In the alias validation flow, move the knownAliases
registration in the alias-handling block after _deployCheckAliasRefs validates
op.args. Keep duplicate and operation-type checks before argument validation,
and only append the alias once references have passed so an operation cannot
reference its own alias.
- Around line 455-473: Compute and store commitStarted from job.commitStatus
before the status reset loop in the resume flow. Keep the existing detection of
any non-pending commit entry, then remove the later recomputation so phase
assignment uses the pre-reset value and resumes failed commits in committing
rather than staging.
- Around line 271-301: Update the in-flight handling around
_deployReconcileCreateOp so an unreconciled create operation is treated as
ambiguous rather than re-run. When reconciliation returns null, mark the entry
interrupted/failed, set the job to failed with guidance to verify the hub and
resume only with retryInFlight=true or cancel, clear the slice lease, save the
job, and stop processing; preserve adoption for successfully reconciled creates
and existing addActions behavior.

In `@libraries/mcp-devices-lib.groovy`:
- Line 4251: Remove the extra space in the “auto-token” wording across all six
output schema descriptions, including the entries associated with opToken.
Ensure each exposed tool metadata string uses “auto-token” consistently.
- Line 4464: Update the opToken description in the hub_call_device_replace
schema to clarify that the server-assigned token applies only to untokened write
calls, excluding the read-only list_options=true mode; preserve the existing
replay guidance.

In `@libraries/mcp-diagnostics-lib.groovy`:
- Line 2300: Update every opToken schema description at
libraries/mcp-diagnostics-lib.groovy lines 2300, 2326, 2358, 2393, 2420, 2448,
2482, and 2531; libraries/mcp-files-lib.groovy lines 396 and 422; and
libraries/mcp-item-backups-lib.groovy lines 859, 889, and 1008, replacing
“Server-assigned auto- token” with “Server-assigned token” while preserving the
rest of each description.

In `@libraries/mcp-item-backups-lib.groovy`:
- Line 889: Update the affected input schemas to advertise the server-consumed
opToken field in libraries/mcp-item-backups-lib.groovy:889,
libraries/mcp-diagnostics-lib.groovy:2300, 2326, 2358, 2393, 2420, 2448, 2482,
2531, and libraries/mcp-files-lib.groovy:396, 422. For schemas with required
operation parameters, represent token-only replay as a valid alternate input
shape so schema-driven clients can submit only opToken; preserve the existing
parameterized input shape.

In `@libraries/mcp-native-rules-lib.groovy`:
- Around line 13241-13290: Update the multi-operation guard around editOpGroups
to reject same-family singular and plural combinations: treat triggerOpNames or
actionOpNames containing more than one operation as invalid, even when they are
the only non-empty group. Preserve the documented addTriggers plus addActions
bulk pair, while ensuring addTrigger+addTriggers and addAction+addActions throw
before dispatch.
- Around line 9426-9431: Update the write-gate logic used by both hub_set_rule
and hub_set_native_app so deployment requests with args.deployment.op equal to
status bypass the write requirement, while deployment mutations remain gated.
Preserve _deployRouteFromTool’s mixed-argument validation and add regression
coverage for deployment status when enableWrite is false in both schema-only
helpers.

In `@libraries/mcp-self-admin-lib.groovy`:
- Around line 94-100: Align maxConcurrentWrites validation across
libraries/mcp-self-admin-lib.groovy lines 94-100 and hubitat-mcp-server.groovy
lines 435-437: keep the input range at 0..100 and update the validation upper
bound accordingly, then state both bounds in the IllegalArgumentException
message. Update the range definition in hubitat-mcp-server.groovy lines 435-437
only if choosing a different bound; otherwise keep range: "0..100" as the single
source of truth.

In `@libraries/mcp-system-lib.groovy`:
- Around line 212-231: Update the includeRecentOps handling around _opTokenDedup
and the recentOps construction to prevent unauthorized replay of write
operations. Apply the existing read/write and per-tool permission checks before
replay, or filter write tokens and their buffered results from info.recentOps
when the client lacks access, while preserving visibility of authorized
operations.

In `@src/test/groovy/server/ToolDeploymentJobsSpec.groovy`:
- Around line 5-22: Remove the multi-paragraph class-level docblock above the
deployment tests in ToolDeploymentJobsSpec. Preserve the test code and retain
only comments that document non-obvious setup constraints, such as atomic state
behavior.

In `@tests/BAT-rm-native-crud.md`:
- Around line 231-236: Update the test_prompt stopped-state assertion to state
that eventSubscriptionCount is numeric and equals 0, matching the expected
health-response contract and the explicit verification in the test expectation.
Keep the start-state assertion that eventSubscriptionCount is greater than 0
unchanged.

In `@tests/e2e_test.py`:
- Around line 3571-3576: Update the stopped-status verification around
_rule_status_when to use hub_get_rule_health as the authoritative source for the
rule’s stopped state. Cross-check or fall back to its stopped field when
hub_list_rules does not report stopped, while preserving the existing label/name
decoration assertions.

---

Nitpick comments:
In `@hubitat-mcp-server.groovy`:
- Around line 1573-1591: Update auto-token generation in the !opTokenActive
block to use a wider random suffix than the current 16-bit nextInt(0xFFFF)
value, while preserving the existing timestamp prefix, formatting, and token
assignment behavior.
- Around line 2137-2180: Add a brief comment near the fingerprint handling in
_canonicalOpArgs or _findIdenticalRunningOp documenting that fpHash plus fpLen
avoids storing canonical JSON, keeps atomicState records bounded, and permits a
small false-positive risk; also mention that duplicate detection is limited to
the 10-minute recency window used by _findIdenticalRunningOp and
_recentRunningWriteOps.
- Around line 1639-1672: Add a regression test in RelayBudgetSpec.groovy for
too_many_writes_in_flight that first verifies refusal, advances or waits until
the blocking in-flight record expires, then re-issues the same client-tokened
call and asserts it executes successfully. Reuse the existing setup and
no-orphan-record assertions, ensuring the expired blocker no longer prevents
retry.

In `@libraries/mcp-deploy-jobs-lib.groovy`:
- Line 407: Update the jobId construction in _deploySaveJob to append a short
uniqueness suffix to the current timestamp, using an existing random or counter
utility where available. Preserve the “dj-” prefix and timestamp while ensuring
same-millisecond create calls produce distinct IDs.
- Around line 290-300: Assign the mutated statusList back to the appropriate
job.opStatus or job.commitStatus field before calling _deploySaveJob(job) in the
addActions gated path. Apply the same assignment in the adopt branch, preserving
the staging-based field selection used by the existing status-list logic.
- Around line 356-366: Update the worker exception handling around
_deployRunSlice to track consecutive slice exceptions on the job record,
incrementing the count and retaining the latest error. When the count exceeds a
small bounded retry limit, set the job phase to "failed" with the last error so
the existing stillActive check stops re-arming; reset the consecutive-exception
count after a successful slice.

In `@libraries/mcp-native-rules-lib.groovy`:
- Around line 7757-7758: Replace the manual deduplication loops for
uniqueApplied, including the corresponding logic at the second occurrence, with
List.unique(false) on applied. Preserve insertion order and avoid mutating the
original applied list.
- Around line 152-161: Update _setRuleFlatTool() to document the deployment
capability in its flat-mode description and inputSchema, matching the existing
deployment definition and behavior exposed by the gateway schema. Ensure
flat-mode callers can discover and submit deployment without changing dispatch
or operation handling.

In `@src/test/groovy/server/RelayBudgetSpec.groovy`:
- Around line 195-220: Add a test alongside the existing client-tokened write
spec that sets maxConcurrentWrites to 1, seeds one running operation, and
invokes a different write with a client opToken. Assert the tool does not run,
the parsed response reports too_many_writes_in_flight, and no client token
record is created, preserving the cap independently of fingerprint duplicate
handling.

In `@src/test/groovy/server/ToolManageFilesSpec.groovy`:
- Around line 57-74: Extend the hub_list_files tests around the existing
“hub_list_files filters file names by case-insensitive substring” case: invoke
the tool through mcpDriver.callTool with filter in both gateway modes, add an
HTML-fallback assertion for case-insensitive filtering, and add
cursor-pagination coverage proving the filter is applied before pagination.

In `@tests/BAT-v2.md`:
- Line 4627: Add a separate goal-framed BAT scenario for mixed bare-argument
batches that verifies the call refuses fail-loud without partial application,
then retries the same changes through patches successfully. Remove this
rejection requirement from T661 so it remains focused solely on in_progress
resume behavior.

In `@tests/e2e_test.py`:
- Around line 3567-3582: Update the stop/start calls in the rule lifecycle test
to use the existing _status_write limiter-recovery pattern, or an equivalent
watchdog bounce-and-single-retry wrapper. Preserve the current hub_call_rule
actions, success assertions, and status polling while ensuring excessive hub
load is retried instead of failing immediately.
- Around line 4187-4191: Update the teardown finally block around _delete_native
to also clean up job_id: if present and unfinished, cancel it first, then delete
the deployment with deployment={"op": "delete", "jobId": job_id}. Catch cleanup
errors so teardown preserves the original test failure, and ensure the job
record is removed to avoid duplicate job_name recovery records.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4900d3d3-3eb3-4f98-b62f-729ff4360800

📥 Commits

Reviewing files that changed from the base of the PR and between be202e8 and e3afa50.

📒 Files selected for processing (32)
  • .github/scripts/e2e_scope.py
  • TOOL_GUIDE.md
  • docs/rm_action_subtype_schemas.md
  • hubitat-mcp-server.groovy
  • libraries/mcp-app-cloner-lib.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • libraries/mcp-custom-rules-lib.groovy
  • libraries/mcp-dashboards-lib.groovy
  • libraries/mcp-debug-logging-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-devices-lib.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-item-backups-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
  • libraries/mcp-rooms-lib.groovy
  • libraries/mcp-self-admin-lib.groovy
  • libraries/mcp-system-lib.groovy
  • libraries/mcp-variables-lib.groovy
  • libraries/mcp-virtual-devices-lib.groovy
  • libraries/mcp-visual-rules-lib.groovy
  • src/test/groovy/server/IncludeResolverSpec.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • tests/BAT-rm-native-crud.md
  • tests/BAT-v2.md
  • tests/e2e_test.py
  • tests/sandbox_lint.py
  • tools/build-bundle.py

Comment thread hubitat-mcp-server.groovy
Comment thread libraries/mcp-app-cloner-lib.groovy Outdated
Comment thread libraries/mcp-code-management-lib.groovy Outdated
Comment thread libraries/mcp-custom-rules-lib.groovy Outdated
Comment thread libraries/mcp-deploy-jobs-lib.groovy Outdated
Comment thread libraries/mcp-self-admin-lib.groovy
Comment thread libraries/mcp-system-lib.groovy
Comment thread src/test/groovy/server/ToolDeploymentJobsSpec.groovy
Comment thread tests/BAT-rm-native-crud.md Outdated
Comment thread tests/e2e_test.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/test/groovy/server/ToolManageFilesSpec.groovy (1)

57-74: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add HTML fallback coverage for the new filter.

This test covers only JSON responses. The existing HTML fallback test at Lines 147-163 does not provide filter, so a regression in filtering HTML-derived filenames could pass. Add an HTML case that uses filter: 'AlP' and verifies the filtered, sorted names.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/groovy/server/ToolManageFilesSpec.groovy` around lines 57 - 74, Add
HTML fallback coverage alongside the existing HTML fallback test, using filter:
'AlP' when calling toolListFiles. Verify that HTML-derived filenames are
filtered case-insensitively and returned in the expected sorted order, matching
the JSON test’s alpha.txt and ALPINE.csv results.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/test/groovy/server/ToolManageFilesSpec.groovy`:
- Around line 57-74: Add HTML fallback coverage alongside the existing HTML
fallback test, using filter: 'AlP' when calling toolListFiles. Verify that
HTML-derived filenames are filtered case-insensitively and returned in the
expected sorted order, matching the JSON test’s alpha.txt and ALPINE.csv
results.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 70fd6099-f3b5-4807-8cdf-ce15b12f0e03

📥 Commits

Reviewing files that changed from the base of the PR and between e3afa50 and 681d3a6.

📒 Files selected for processing (7)
  • libraries/mcp-discovery-lib.groovy
  • src/test/groovy/server/ToolAppDriverCodeSpec.groovy
  • src/test/groovy/server/ToolLibraryCodeSpec.groovy
  • src/test/groovy/server/ToolListRmRulesSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolVisualRuleRestoreSpec.groovy
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy

…erage, schema consistency

Engine: alias self-reference fails at validation; unreconcilable
interrupted create ops gate behind retryInFlight instead of re-running
(duplicate-app risk); commitStarted read before the resume reset loop;
lease guards on cancel/delete; statusList written back on gated paths;
worker re-arm bounded by a fail streak; jobId carries a random suffix.

Gates: deployment op=status recognized by both schema-only helpers so it
stays readable with the Write master off; the token-replay short-circuit
and the recentOps journal now enforce the masters.

Schema/docs: every write tool that returns an opToken now advertises it
as input, unified on the FLAT_TRIM'd wording (flat catalog stays ~116KB);
auto-token wording fixed; guard extended to same-family singular+plural;
maxConcurrentWrites bounds aligned with the settings page; flat-mode
hub_set_rule surfaces deployment; e2e stop/start verifies via
hub_get_rule_health (authoritative) with limiter recovery.
…2e write-cap backpressure

The e2e hub exposed the contract gap: a schedule-only rule carries NO
eventSubscriptions section in a perfectly readable statusJson, so
absent-section must read 0, with null reserved for the fetch failing.
Stopped rules therefore read 0 too (they ARE at zero live subscriptions).
T311 and the lifecycle e2e updated to the corrected contract; the start
leg of a time-triggered rule asserts the re-armed schedule, not a
subscription it never had.

The run also proved the write cap refuses a strictly-serial client:
severed responses leave hub-side writes running for minutes, so the
runner is 'the third writer'. call_tool now treats
too_many_writes_in_flight as the designed backpressure signal --
wait-and-re-issue bounded just past the cap's 10-minute record window.

Also tightens the 20 opToken input descriptions to the FLAT_TRIM pattern
with the handle lifetime stated per the MCP 2026-07-28 Stateful Tools
guidance (flat catalog: 27 B visible per site, was 303).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/test/groovy/server/ToolRmNativeCrudSpec.groovy (1)

4127-4127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove this redundant comment.

Line 4127 does not explain a non-obvious reason. The test name already identifies the action-family case.

As per coding guidelines, comments should explain only non-obvious reasons.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/groovy/server/ToolRmNativeCrudSpec.groovy` at line 4127, Remove the
redundant comment immediately preceding the action-family test case; leave the
test and surrounding trigger guard unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libraries/mcp-deploy-jobs-lib.groovy`:
- Around line 437-439: Update the jobId generation in the deployment job
creation flow near _deploySaveJob to use java.util.UUID.randomUUID() before the
first save, replacing the 16-bit Random suffix. Preserve the existing
timestamp-based prefix while ensuring IDs are collision-resistant so
atomicState.deployJobs entries are not overwritten.

In `@libraries/mcp-files-lib.groovy`:
- Line 389: Update the opToken descriptions in both schema definitions at the
visible entries near lines 389 and 416 so “8-128 chars, A-Za-z0-9._-” appears
before [[FLAT_TRIM]], keeping the token format visible in flat mode while
preserving the remaining guidance.

---

Nitpick comments:
In `@src/test/groovy/server/ToolRmNativeCrudSpec.groovy`:
- Line 4127: Remove the redundant comment immediately preceding the
action-family test case; leave the test and surrounding trigger guard unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e2f7b40-50ba-4cc1-8970-f5e8101053d3

📥 Commits

Reviewing files that changed from the base of the PR and between 681d3a6 and 1c26fb3.

📒 Files selected for processing (29)
  • hubitat-mcp-server.groovy
  • libraries/mcp-app-cloner-lib.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • libraries/mcp-custom-rules-lib.groovy
  • libraries/mcp-dashboards-lib.groovy
  • libraries/mcp-debug-logging-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-devices-lib.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-item-backups-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
  • libraries/mcp-rooms-lib.groovy
  • libraries/mcp-self-admin-lib.groovy
  • libraries/mcp-system-lib.groovy
  • libraries/mcp-variables-lib.groovy
  • libraries/mcp-virtual-devices-lib.groovy
  • libraries/mcp-visual-rules-lib.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/SetRuleSelfGatewaySpec.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy
  • tests/BAT-rm-native-crud.md
  • tests/e2e_test.py
🚧 Files skipped from review as they are similar to previous changes (18)
  • libraries/mcp-debug-logging-lib.groovy
  • libraries/mcp-virtual-devices-lib.groovy
  • libraries/mcp-rooms-lib.groovy
  • libraries/mcp-variables-lib.groovy
  • libraries/mcp-custom-rules-lib.groovy
  • libraries/mcp-self-admin-lib.groovy
  • tests/BAT-rm-native-crud.md
  • libraries/mcp-code-management-lib.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-app-cloner-lib.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • libraries/mcp-system-lib.groovy
  • libraries/mcp-dashboards-lib.groovy
  • libraries/mcp-devices-lib.groovy
  • libraries/mcp-item-backups-lib.groovy
  • hubitat-mcp-server.groovy
  • libraries/mcp-visual-rules-lib.groovy
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: test (strict, gateway)
  • GitHub Check: test (strict, flat)
  • GitHub Check: test (normal, gateway)
  • GitHub Check: test (normal, flat)
  • GitHub Check: groovy2x-spock (allow-failure)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.groovy: Use atomicState for thread-safe persistence, state for UI/counters, and compare device IDs as strings with toString().
Add comments only when explaining non-obvious rationale; avoid multi-paragraph docblocks and references to the current PR, issue, or caller.
Do not ship deprecation aliases when renaming non-conforming MCP tools; rename clients and server references in lockstep.
Use unambiguous parameter names such as device_id and user_id, naming complex parameters after their semantic value.
Merge opposite state mutations into one set__ tool, merge filter/projection variants with optional parameters, and avoid consolidation when error modes, safety gates, or payload shapes differ fundamentally.
Every new MCP tool must explicitly classify readOnlyHint, idempotentHint, and openWorldHint; emit destructiveHint for writes and omit it for reads.
Every leaf tool and gateway must have display metadata with a unique Title-Case title and a one-sentence summary of at most 140 characters ending in a period.
Keep annotation hints advisory; enforce read/write permissions through the universal master gates and destructive operations through confirmation and backup checks.
Tool descriptions must begin with a concise summary, include usage and safety guidance, expose implicit constraints, and keep examples in parameter descriptions or the served guide rather than stuffing them into the body.
Use inputSchema objects, enums for fixed values, omit required when no parameters are mandatory, and declare an outputSchema on every new tool definition with nullable fields modeled accurately.
When output schemas are published, return structuredContent for every successful advertised tool result and emit the wire-form schema with recursive required fields removed.
Validation failures must throw IllegalArgumentException before any side effect; runtime operation failures must return a structured [success:false, error, note] result and use isError:true in the MCP...

Files:

  • src/test/groovy/server/SetRuleSelfGatewaySpec.groovy
  • src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
src/test/groovy/**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

Every new MCP tool requires both a direct-call unit test and a dispatch-envelope integration test.

Files:

  • src/test/groovy/server/SetRuleSelfGatewaySpec.groovy
  • src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • src/test/groovy/server/SetRuleSelfGatewaySpec.groovy
  • src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • tests/e2e_test.py
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • src/test/groovy/server/SetRuleSelfGatewaySpec.groovy
  • src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • tests/e2e_test.py
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
libraries/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

libraries/*.groovy: The library(...) declaration must be the first line; keep comments inside methods, use string-literal subscribe/schedule handlers, and do not place preferences, mappings, or file-scope closures in libraries.
Keep each tool's definitions, implementation, domain helpers, classification metadata, and display metadata in its domain library; do not cross-include libraries.

libraries/*.groovy: Library files must begin with the library(...) declaration, keep comments inside methods, use string-literal subscribe/schedule handlers, and avoid preferences, mappings, or file-scope closures.
Per-tool definitions, implementations, domain helpers, classifications, and display metadata belong in the domain library; gateway membership and dispatch cases remain in the main app; libraries must not cross-include one another.

Files:

  • libraries/mcp-files-lib.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
tests/e2e_test.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/e2e_test.py: Add or update e2e coverage for observable tool, dispatch, gateway, transport/protocol, and bug-fix behavior; keep tool names and scenarios synchronized with the server.
Keep Rule Machine e2e scenarios small and grouped by concern; never soft-skip wire-format assertions on relay 504s.

Changes observable to live MCP clients, including tool behavior, dispatch, gateways, transport, and bug fixes, require an updated e2e scenario; keep renamed tools and scripts synchronized.

Files:

  • tests/e2e_test.py
🔇 Additional comments (14)
src/test/groovy/server/SetRuleSelfGatewaySpec.groovy (1)

40-40: LGTM!

src/test/groovy/server/HubInfoFieldContractSpec.groovy (1)

98-134: LGTM!

src/test/groovy/server/OpTokenReplaySpec.groovy (1)

153-180: LGTM!

tests/e2e_test.py (1)

420-456: LGTM!

Also applies to: 3504-3532, 3595-3631, 3644-3672, 3810-3825, 4195-4289, 5911-5942

libraries/mcp-diagnostics-lib.groovy (1)

2295-2302: LGTM!

Also applies to: 2322-2329, 2355-2362, 2390-2398, 2418-2426, 2447-2455, 2482-2490, 2533-2540

libraries/mcp-deploy-jobs-lib.groovy (1)

1-9: LGTM!

Also applies to: 49-53, 277-313, 377-389, 487-490, 564-569, 705-709, 723-747

libraries/mcp-native-rules-lib.groovy (4)

9270-9274: LGTM!

Also applies to: 9309-9312, 9438-9443, 9522-9526, 13253-13304


8-8: LGTM!

Also applies to: 24-24, 674-674, 698-698, 818-834, 860-872, 15052-15076


50-57: LGTM!

Also applies to: 87-87, 115-115, 139-141, 154-163, 213-213, 285-295, 389-389, 415-417, 466-466, 492-492, 9248-9256


7759-7770: LGTM!

Also applies to: 9073-9075

libraries/mcp-files-lib.groovy (1)

6-7: LGTM!

Also applies to: 79-82, 111-114, 326-330, 397-397, 424-424

src/test/groovy/server/ToolManageFilesSpec.groovy (1)

57-145: LGTM!

Also applies to: 471-471, 692-692, 737-737

src/test/groovy/server/ToolRmNativeCrudSpec.groovy (1)

4096-4126: LGTM!

Also applies to: 4128-4152

src/test/groovy/server/UpdateNativeAppSchemaTrimSpec.groovy (1)

170-170: LGTM!

Comment thread libraries/mcp-deploy-jobs-lib.groovy Outdated
Comment thread libraries/mcp-files-lib.groovy Outdated
Two deployment creates in the same millisecond could share a jobId and
the second would overwrite the first's record. UUID.randomUUID (sandbox
allow-listed) removes the case.

A flat-mode client could not see the token format (8-128 chars,
A-Za-z0-9._-) -- it sat inside FLAT_TRIM against the critical-formats
rule. The format now leads the description on all 30 opToken input
sites (flat-visible cost: ~28 bytes per site).
…as writes

The auto-token gate and the cap's isWriteLeaf classified purely by
getReadOnlyToolNames(), so deployment op='status' polls and guide/discover
probes minted running write records, counted toward maxConcurrentWrites
(a status poll could be refused by the cap it exists to help diagnose),
churned the journal, and two concurrent identical polls could hit
duplicate_in_flight. Both sites now consult the same schema-only
classifiers the Write master uses.
…rage

Engine: a create that COMMITTED but reported failure (stageDisabled leg)
now records its app so cancel rolls it back and resume gates behind
retryInFlight instead of duplicating; reconcile adopts only on a label
match; cancel holds the slice lease across its delete loop; the worker
skips background:false jobs and initialize() re-arms it after the blanket
unschedule(); failed jobs are never cap-evicted (their createdAppIds/
backupKeys are the rollback handles); manifests are bounded (50 ops,
64KB); setDisabled requires an explicit boolean; worker throws are
journaled from the first failure; envelopes report success honestly
(failed job, failed rollback) plus background/workerScheduled.

Guard: replaceActions joins the multi-op guard; the refusal text stops
claiming a silent drop for the shared-branch combos and names both tools.

Health: stopped reads statusJson's own state.stopped (the field the
stop/start toggle already trusts), markup decoration decides when
statusJson is unreadable, plain suffixes never false-flag a rule named
'(Stopped)'; list path uses the paused remainder-diff; labels keep one
encoding; statusJson read failures land in checkErrors.

Files: op-result buffers hidden from hub_list_files by default
(includeOpResults:true shows them). recentOpsLimit validates strictly.
Flat hub_set_rule schema no longer requires operation alongside a
self-contained deployment. Docs: draft:true/includeOps documented,
backup gate stated, refusal statuses re-homed, T465 op shape fixed,
T311 goal-framed.

Tests: 20+ new engine specs (worker streak, cap/prune, adoption, lease,
confirm gates, honest envelopes), dispatch-envelope tests through
mcpDriver.callTool pinning the read-shaped classifier, settingsApplied
dedupe pin, cap default pin, stopped-detection matrix, maxConcurrentWrites
validation, list_files exclusion.
@kingpanther13 kingpanther13 added the release:minor Minor version bump on merge (multiple new tools, architectural change) label Aug 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
libraries/mcp-deploy-jobs-lib.groovy (1)

697-737: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Expose job.cancel in the status output.

_deployOpCancel returns cancel and success:false on its own response only. _deployJobStatus never reads job.cancel, so a later deployment:{op:'status', jobId} on a partially rolled-back job reports phase:"cancelled" and success:true with no mention of the apps that could not be deleted. The operator loses the residual-cleanup list as soon as the cancel response is gone.

🐛 Proposed fix
     if (job.validation != null) out.validation = job.validation
+    if (job.cancel instanceof Map) {
+        out.cancel = job.cancel
+        if (((job.cancel.failures ?: []) as List)) {
+            out.success = false
+            out.error = "Rollback incomplete: ${((List) job.cancel.failures).size()} created app(s) could not be deleted."
+        }
+    }
     // jobError stays for compatibility; error is the runtime-error contract's field.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libraries/mcp-deploy-jobs-lib.groovy` around lines 697 - 737, Update
_deployJobStatus to include the persisted job.cancel data in its status map,
preserving the cancellation result—including success:false and any residual
cleanup details—when later status requests report a cancelled or partially
rolled-back job.
🧹 Nitpick comments (1)
src/test/groovy/server/ToolDeploymentJobsSpec.groovy (1)

347-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for maxOpsPerCall slice bounding.

The worker tests cover throwing slices, streak reset, and the background:false opt-out. No test drives a job with maxOpsPerCall set, so the bounded-slice path in _deployRunSlice (line 293) and the resulting workerScheduled:true continuation are unverified. That path is the durability claim of this layer. A create with three ops and maxOpsPerCall:1 should stop after one op, report phase:"staging" and workerScheduled == true, then finish across subsequent deployJobWorker() calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/groovy/server/ToolDeploymentJobsSpec.groovy` around lines 347 - 414,
Add a test in the worker slice coverage around deployJobWorker that seeds a job
with three operations and maxOpsPerCall set to 1, then verifies the first
deployJobWorker call processes only one operation while preserving phase
"staging" and setting workerScheduled true. Invoke deployJobWorker for the
remaining continuations and assert the job eventually completes, covering the
bounded _deployRunSlice path and scheduled continuation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@libraries/mcp-files-lib.groovy`:
- Around line 8-10: Update the includeOpResults validation in the
argument-processing flow to distinguish an omitted key from an explicitly
supplied value. Accept only Boolean values, throw IllegalArgumentException for
explicit non-Boolean inputs such as strings, numbers, or null, and perform this
validation before any File Manager calls; preserve false as the default when the
key is omitted.

In `@src/test/groovy/server/ToolRmNativeCrudSpec.groovy`:
- Around line 4154-4178: Update the nonexistent-app preflight test around
toolSetRule to record every uploadHubFile invocation in a list instead of
silently discarding calls. Add an assertion that this upload-call list is empty,
alongside the existing posts.isEmpty() check, so the test verifies no backup
file is written before the 404 steer.

---

Outside diff comments:
In `@libraries/mcp-deploy-jobs-lib.groovy`:
- Around line 697-737: Update _deployJobStatus to include the persisted
job.cancel data in its status map, preserving the cancellation result—including
success:false and any residual cleanup details—when later status requests report
a cancelled or partially rolled-back job.

---

Nitpick comments:
In `@src/test/groovy/server/ToolDeploymentJobsSpec.groovy`:
- Around line 347-414: Add a test in the worker slice coverage around
deployJobWorker that seeds a job with three operations and maxOpsPerCall set to
1, then verifies the first deployJobWorker call processes only one operation
while preserving phase "staging" and setting workerScheduled true. Invoke
deployJobWorker for the remaining continuations and assert the job eventually
completes, covering the bounded _deployRunSlice path and scheduled continuation
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 00a11eec-517b-4d10-b9f3-01aecef04c7c

📥 Commits

Reviewing files that changed from the base of the PR and between 1c26fb3 and f70cca9.

📒 Files selected for processing (20)
  • .github/scripts/e2e_scope.py
  • TOOL_GUIDE.md
  • hubitat-mcp-server.groovy
  • libraries/mcp-app-cloner-lib.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-item-backups-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
  • libraries/mcp-self-admin-lib.groovy
  • libraries/mcp-system-lib.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy
  • tests/BAT-rm-native-crud.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • .github/scripts/e2e_scope.py
  • tests/BAT-rm-native-crud.md
  • libraries/mcp-item-backups-lib.groovy
  • libraries/mcp-app-cloner-lib.groovy
  • libraries/mcp-self-admin-lib.groovy
  • libraries/mcp-system-lib.groovy
  • libraries/mcp-diagnostics-lib.groovy
  • libraries/mcp-native-rules-lib.groovy
  • TOOL_GUIDE.md
  • hubitat-mcp-server.groovy
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: test (normal, gateway)
  • GitHub Check: test (strict, gateway)
  • GitHub Check: test (strict, flat)
  • GitHub Check: groovy2x-spock (allow-failure)
  • GitHub Check: test (normal, flat)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.groovy: Use atomicState for thread-safe persistence, state for UI/counters, and compare device IDs as strings with toString().
Add comments only when explaining non-obvious rationale; avoid multi-paragraph docblocks and references to the current PR, issue, or caller.
Do not ship deprecation aliases when renaming non-conforming MCP tools; rename clients and server references in lockstep.
Use unambiguous parameter names such as device_id and user_id, naming complex parameters after their semantic value.
Merge opposite state mutations into one set__ tool, merge filter/projection variants with optional parameters, and avoid consolidation when error modes, safety gates, or payload shapes differ fundamentally.
Every new MCP tool must explicitly classify readOnlyHint, idempotentHint, and openWorldHint; emit destructiveHint for writes and omit it for reads.
Every leaf tool and gateway must have display metadata with a unique Title-Case title and a one-sentence summary of at most 140 characters ending in a period.
Keep annotation hints advisory; enforce read/write permissions through the universal master gates and destructive operations through confirmation and backup checks.
Tool descriptions must begin with a concise summary, include usage and safety guidance, expose implicit constraints, and keep examples in parameter descriptions or the served guide rather than stuffing them into the body.
Use inputSchema objects, enums for fixed values, omit required when no parameters are mandatory, and declare an outputSchema on every new tool definition with nullable fields modeled accurately.
When output schemas are published, return structuredContent for every successful advertised tool result and emit the wire-form schema with recursive required fields removed.
Validation failures must throw IllegalArgumentException before any side effect; runtime operation failures must return a structured [success:false, error, note] result and use isError:true in the MCP...

Files:

  • src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
src/test/groovy/**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

Every new MCP tool requires both a direct-call unit test and a dispatch-envelope integration test.

Files:

  • src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy
  • src/test/groovy/server/HubInfoFieldContractSpec.groovy
  • libraries/mcp-files-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • src/test/groovy/server/RelayBudgetSpec.groovy
  • src/test/groovy/server/ToolManageFilesSpec.groovy
  • src/test/groovy/server/ToolRmNativeCrudSpec.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
  • src/test/groovy/server/ToolDeploymentJobsSpec.groovy
libraries/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

libraries/*.groovy: The library(...) declaration must be the first line; keep comments inside methods, use string-literal subscribe/schedule handlers, and do not place preferences, mappings, or file-scope closures in libraries.
Keep each tool's definitions, implementation, domain helpers, classification metadata, and display metadata in its domain library; do not cross-include libraries.

libraries/*.groovy: Library files must begin with the library(...) declaration, keep comments inside methods, use string-literal subscribe/schedule handlers, and avoid preferences, mappings, or file-scope closures.
Per-tool definitions, implementations, domain helpers, classifications, and display metadata belong in the domain library; gateway membership and dispatch cases remain in the main app; libraries must not cross-include one another.

Files:

  • libraries/mcp-files-lib.groovy
  • libraries/mcp-code-management-lib.groovy
  • libraries/mcp-bundles-lib.groovy
  • libraries/mcp-deploy-jobs-lib.groovy
🔇 Additional comments (35)
src/test/groovy/server/ToolUpdateMcpSettingsSpec.groovy (2)

548-568: LGTM!


570-581: LGTM!

src/test/groovy/server/HubInfoFieldContractSpec.groovy (1)

136-158: LGTM!

Also applies to: 160-180

src/test/groovy/server/RelayBudgetSpec.groovy (1)

39-44: LGTM!

Also applies to: 81-92, 96-117, 119-147, 149-176, 178-206, 208-240, 242-269, 271-297

libraries/mcp-bundles-lib.groovy (1)

392-399: LGTM!

Also applies to: 441-449, 470-477

libraries/mcp-code-management-lib.groovy (1)

2764-2771: LGTM!

Also applies to: 2822-2829, 2878-2885, 2943-2950, 2991-2999, 3032-3039, 3074-3081

libraries/mcp-files-lib.groovy (1)

82-88: LGTM!

Also applies to: 117-123, 335-340, 399-407, 426-434

libraries/mcp-deploy-jobs-lib.groovy (13)

40-58: LGTM!


60-68: LGTM!


99-151: LGTM!


153-221: LGTM!


223-268: LGTM!


270-401: LGTM!


403-452: LGTM!


467-501: LGTM!


502-532: LGTM!


534-623: LGTM!


625-684: LGTM!


756-788: LGTM!


790-835: LGTM!

src/test/groovy/server/ToolDeploymentJobsSpec.groovy (5)

29-47: LGTM!


49-179: LGTM!

Also applies to: 181-277


279-345: LGTM!

Also applies to: 416-490


492-596: LGTM!

Also applies to: 598-688


690-750: LGTM!

Also applies to: 752-820

src/test/groovy/server/ToolManageFilesSpec.groovy (7)

57-80: LGTM!


82-98: LGTM!

Also applies to: 100-124


126-143: LGTM!


145-170: LGTM!


496-496: LGTM!


717-717: LGTM!


762-762: LGTM!

src/test/groovy/server/ToolRmNativeCrudSpec.groovy (3)

4181-4227: LGTM!


9422-9514: LGTM!


15116-15180: LGTM!

Comment thread libraries/mcp-files-lib.groovy
Comment thread src/test/groovy/server/ToolRmNativeCrudSpec.groovy
…ateMapValue

The checkpoint save assumed atomicState.updateMapValue exists (firmware
2.3.2+); _opTokenPut already carries the MissingMethodException fallback
for exactly this. Also: includeOpResults validates strictly, and the 404
preflight test pins that no backup upload happened either.
…equest

Root cause of the e2e regression, measured: _opTokenPrune runs on every
tokened call, and past 100 records it batch-evicts ~50 -- deleting each
result file with a synchronous HTTP call. Before auto-tokens only
client-tokened calls made records, so that batch was rare; now every
write makes one, so the eviction fires repeatedly and whichever write
triggers it eats ~50 deletions. A sub-second hub_delete_variable took
7.7-10.4s (baseline: zero slow variable ops), the median hub_set_rule
edit moved 9.5s -> 10.3s across the ~10s relay ceiling, and calls at the
ceiling doubled (33 -> 74). Common-test time went 17.3 -> 37.0 min.

The prune now queues the deterministic filenames and schedules
opTokenFileSweep, which deletes them 20 per pass outside any request,
re-arming while work remains; the queue is capped at 500.

Two compounding fixes: the write-cap window drops 600s -> 90s, so a
record orphaned by a severed write (its hub-side thread dies before the
terminal path, leaving it 'running' forever) can no longer wedge every
write for ten minutes -- that wedge cost 82 refusals and ~14 minutes of
client waiting in this run. And the per-request gateway/read-only
aggregator rebuilds are hoisted from nine calls to two.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/test/groovy/server/OpTokenReplaySpec.groovy`:
- Around line 893-899: Update the test around opTokenFileSweep() to assert the
intermediate batch state immediately after the second sweep: deleted must
contain 40 files and atomicStateMap.opTokenSweepFiles must contain 12 queued
files, before performing any further sweep or final drain assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 71840725-6970-442c-86ef-88166b3fe503

📥 Commits

Reviewing files that changed from the base of the PR and between 894102f and c62dfff.

📒 Files selected for processing (3)
  • hubitat-mcp-server.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • tests/e2e_test.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/e2e_test.py
  • hubitat-mcp-server.groovy
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: e2e (run)
  • GitHub Check: test (strict, gateway)
  • GitHub Check: test (normal, flat)
  • GitHub Check: test (strict, flat)
  • GitHub Check: test (normal, gateway)
  • GitHub Check: groovy2x-spock (allow-failure)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.groovy: Use atomicState for thread-safe persistence, state for UI/counters, and compare device IDs as strings with toString().
Add comments only when explaining non-obvious rationale; avoid multi-paragraph docblocks and references to the current PR, issue, or caller.
Do not ship deprecation aliases when renaming non-conforming MCP tools; rename clients and server references in lockstep.
Use unambiguous parameter names such as device_id and user_id, naming complex parameters after their semantic value.
Merge opposite state mutations into one set__ tool, merge filter/projection variants with optional parameters, and avoid consolidation when error modes, safety gates, or payload shapes differ fundamentally.
Every new MCP tool must explicitly classify readOnlyHint, idempotentHint, and openWorldHint; emit destructiveHint for writes and omit it for reads.
Every leaf tool and gateway must have display metadata with a unique Title-Case title and a one-sentence summary of at most 140 characters ending in a period.
Keep annotation hints advisory; enforce read/write permissions through the universal master gates and destructive operations through confirmation and backup checks.
Tool descriptions must begin with a concise summary, include usage and safety guidance, expose implicit constraints, and keep examples in parameter descriptions or the served guide rather than stuffing them into the body.
Use inputSchema objects, enums for fixed values, omit required when no parameters are mandatory, and declare an outputSchema on every new tool definition with nullable fields modeled accurately.
When output schemas are published, return structuredContent for every successful advertised tool result and emit the wire-form schema with recursive required fields removed.
Validation failures must throw IllegalArgumentException before any side effect; runtime operation failures must return a structured [success:false, error, note] result and use isError:true in the MCP...

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
src/test/groovy/**/*.groovy

📄 CodeRabbit inference engine (CLAUDE.md)

Every new MCP tool requires both a direct-call unit test and a dispatch-envelope integration test.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • src/test/groovy/server/OpTokenReplaySpec.groovy
🔇 Additional comments (5)
src/test/groovy/server/OpTokenReplaySpec.groovy (5)

153-180: LGTM!


848-862: LGTM!


864-892: LGTM!


902-917: LGTM!


959-961: LGTM!

Comment thread src/test/groovy/server/OpTokenReplaySpec.groovy Outdated
The sweep queue stores these names in atomicState, and a GString never
equals the String it renders as -- List.contains and any other value
comparison against a stored name silently answers false (caught by the
Groovy 2.5 lane, where the spec's contains() check failed against a list
that visibly held the name).
It lowered the cap to 1 BEFORE creating its rule, so its own setup writes then
queued behind the background leg -- four BACKPRESSURE waits in the last run --
and a 504 on the finally's cap restore failed the whole test. Create the rule
first, lower the cap second, and re-send the cap writes on a dropped response
(they are idempotent settings assignments).
Independent analysis (codex) reached the same conclusion and declined to rewrite
it: the first write replaces its own `running` marker with `complete` in its
handler's finally, and a second request cannot enter the app until that handler
returns. By the time the journal is inspected there is no running record, so the
refusal cannot fire; the relay expiring the queued request is the 504 the last
two runs died on. Seeding a running record, as RelayBudgetSpec does
deterministically, has no public API.

The contract keeps its unit coverage (RelayBudgetSpec, both directions incl.
maxConcurrentWrites=0). The suite-pin comment now records WHY there is no live
test instead of pointing at one.
My previous commit claimed the refusal was unreachable live. That was wrong, and
the analysis behind it was steered -- I handed the reviewing agent my own theory
and invited it to agree. Re-asked without that framing, it produced this, which
works for reasons my version missed:

- it drains any pre-existing running markers first, so a stale marker from an
  earlier lost-response write cannot occupy the cap slot or impersonate the
  overlap (that contention is what produced the BACKPRESSURE waits);
- it synchronises on the SERVER's marker rather than a sleep, polling the first
  token until it reads running, so the second write provably arrives during the
  overlap;
- it asserts the refusal names that first token in inFlight, so the refusal must
  come from this write and not a leftover record.

The refusal itself was never in doubt: it was observed live on a hub, and fired
34 times on the 2026-08-09 lane -- which is why the suite pins the cap to 0.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e_test.py`:
- Around line 6095-6107: Update the post-loop assertion following the marker
synchronization loop to accept either marker.get("status") == "running" or
marker.get("replayed") is True, matching both valid break conditions already
handled in that loop. Preserve the existing diagnostic message and polling
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7e48ffe0-2621-488d-8b93-65e106145004

📥 Commits

Reviewing files that changed from the base of the PR and between 9ee7a31 and 949ccc9.

📒 Files selected for processing (1)
  • tests/e2e_test.py
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: e2e (run)
  • GitHub Check: groovy2x-spock
  • GitHub Check: test (strict, gateway)
  • GitHub Check: test (normal, flat)
  • GitHub Check: test (strict, flat)
  • GitHub Check: test (normal, gateway)
🧰 Additional context used
📓 Path-based instructions (3)
tests/e2e_test.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/e2e_test.py: Add or update e2e coverage for observable tool, dispatch, gateway, transport/protocol, and bug-fix behavior; keep tool names and scenarios synchronized with the server.
Keep Rule Machine e2e scenarios small and grouped by concern; never soft-skip wire-format assertions on relay 504s.

Changes observable to live MCP clients, including tool behavior, dispatch, gateways, transport, and bug fixes, require an updated e2e scenario; keep renamed tools and scripts synchronized.

Files:

  • tests/e2e_test.py
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • tests/e2e_test.py
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • tests/e2e_test.py
🔇 Additional comments (3)
tests/e2e_test.py (3)

28-28: LGTM!


6038-6144: LGTM! The rest of the new test_write_cap_refuses_a_concurrent_marker_tracked_write correctly drains stale running markers before synchronizing on a fresh one, raises and later restores maxConcurrentWrites, asserts the refusal names the in-flight token, resolves the background write to a replayable terminal state, and cleans up the background client session and the native rule fixture in finally.


12708-12710: LGTM!

Comment thread tests/e2e_test.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_e2e_test_helpers.py (1)

1-8: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Exercise the JSON-string gateway argument path.

The test passes args as a dictionary. It does not detect regressions in JSON decoding. Pass a JSON string instead.

Proposed test adjustment
-    assert et._op_key("hub_manage_rule_machine", {"tool": "hub_set_rule", "args": {}}) == "hub_set_rule:create"
+    assert et._op_key("hub_manage_rule_machine", {
+        "tool": "hub_set_rule",
+        "args": json.dumps({}),
+    }) == "hub_set_rule:create"

Also applies to: 271-273

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_e2e_test_helpers.py` around lines 1 - 8, Update the affected test
cases in tests/test_e2e_test_helpers.py to pass the gateway args as a
JSON-encoded string rather than a dictionary, exercising the JSON decoding path
while preserving the existing expected behavior and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/test_e2e_test_helpers.py`:
- Around line 1-8: Update the affected test cases in
tests/test_e2e_test_helpers.py to pass the gateway args as a JSON-encoded string
rather than a dictionary, exercising the JSON decoding path while preserving the
existing expected behavior and assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c03dd2d-13d3-4a39-a57e-244f0699fdfa

📥 Commits

Reviewing files that changed from the base of the PR and between 929192d and 1ab8e5c.

📒 Files selected for processing (4)
  • hubitat-mcp-server.groovy
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • tests/e2e_test.py
  • tests/test_e2e_test_helpers.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/groovy/server/OpTokenReplaySpec.groovy
  • tests/e2e_test.py
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
  • GitHub Check: e2e (run)
  • GitHub Check: groovy2x-spock
  • GitHub Check: test (normal, gateway)
  • GitHub Check: test (strict, flat)
  • GitHub Check: test (normal, flat)
  • GitHub Check: test (strict, gateway)
🧰 Additional context used
📓 Path-based instructions (2)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

**/*: Apply the Boy Scout rule for small adjacent improvements within the touched area, but ask the user before incorporating large fixes or changes spanning separate subsystems.
Ask before force-pushing, deleting branches, bypassing hooks or signing, editing other contributors' PR descriptions, or touching Z-Wave/Zigbee radios on a live test hub.

**/*: Follow the Boy Scout rule by fixing small adjacent issues in touched files, but ask the user before including large fixes that broaden the review surface.
Never edit protected version, release-bookkeeping, changelog, or PR-guard-controlled content; ask first before destructive Git operations, editing others' PRs, bypassing hooks, or touching live Z-Wave/Zigbee radios.

Files:

  • tests/test_e2e_test_helpers.py
**/*.{groovy,py}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./gradlew test and python tests/sandbox_lint.py before pushing.

Files:

  • tests/test_e2e_test_helpers.py
🧠 Learnings (4)
📓 Common learnings
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 378
File: libraries/mcp-deploy-jobs-lib.groovy:555-571
Timestamp: 2026-08-09T12:53:41.770Z
Learning: In `libraries/mcp-deploy-jobs-lib.groovy`, `atomicState.updateMapValue` is used for per-job deployment checkpoints, but its null-value deletion semantics are undocumented. `_deployOpDelete` therefore rebuilds `atomicState.deployJobs` only for terminal deployment jobs, consistent with the existing terminal-job pruning pattern. Lease guards must prevent cancellation or deletion while an active deployment worker holds `sliceLeaseUntil`, because the worker saves a whole-job snapshot.
Learnt from: kingpanther13
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: :0-0
Timestamp: 2026-08-10T17:02:34.806Z
Learning: In `hubitat-mcp-server.groovy`, token-only operation-result recovery is handled by `_isOpTokenPollShape` in `handleToolsCall` before gateway required-parameter enforcement. `requiredParamsByTool()` is derived from each tool definition’s `inputSchema.required`; preserve these required declarations so the catalog accurately advertises required tool arguments and poll-shape detection remains aligned with runtime validation.
📚 Learning: 2026-08-08T03:29:24.112Z
Learnt from: CR
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-08-08T03:29:24.112Z
Learning: Applies to tests/e2e_test.py : Add or update e2e coverage for observable tool, dispatch, gateway, transport/protocol, and bug-fix behavior; keep tool names and scenarios synchronized with the server.

Applied to files:

  • tests/test_e2e_test_helpers.py
📚 Learning: 2026-08-08T03:29:37.786Z
Learnt from: CR
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-08-08T03:29:37.786Z
Learning: Applies to tests/e2e_test.py : Changes observable to live MCP clients, including tool behavior, dispatch, gateways, transport, and bug fixes, require an updated e2e scenario; keep renamed tools and scripts synchronized.

Applied to files:

  • tests/test_e2e_test_helpers.py
📚 Learning: 2026-08-08T03:29:24.112Z
Learnt from: CR
Repo: kingpanther13/Hubitat-local-MCP-server PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-08-08T03:29:24.112Z
Learning: Applies to tests/e2e_test.py : Keep Rule Machine e2e scenarios small and grouped by concern; never soft-skip wire-format assertions on relay 504s.

Applied to files:

  • tests/test_e2e_test_helpers.py
🔇 Additional comments (3)
tests/test_e2e_test_helpers.py (3)

23-112: LGTM!

Also applies to: 113-130


113-181: 📐 Maintainability & Code Quality

Run the required repository checks before push.

Run ./gradlew test and python tests/sandbox_lint.py before pushing this change.

As per coding guidelines: **/*.{groovy,py} requires ./gradlew test and python tests/sandbox_lint.py before pushing.

Source: Coding guidelines


133-181: 🗄️ Data Integrity & Integration

Likely an incorrect or invalid review comment.

@kingpanther13

Copy link
Copy Markdown
Owner Author

Superseded by #388, which retains the useful fixes from this PR while replacing the custom continuation protocol with standard MCP requestState handling. The branch is intentionally preserved.

kingpanther13 added a commit that referenced this pull request Aug 15, 2026
…R from new mcp spec (#388)

## Summary

- Replace custom client-supplied operation tokens and deployment-job
recovery with MCP 2026-07-28 `requestState` continuations over
Streamable HTTP.
- Keep long native Rule Machine/app writes below the cloud relay ceiling
by handing claimed work to an internal Hubitat worker, while retaining
duplicate prevention and the global concurrent-write cap.
- Make the global write cap memory-only overload protection: no per-call
durable lease ledger (two hub-DB round trips on every write call
removed), same refusal contract and TTL anti-wedge aging, background
work still counted through its own durable records.
- Serve the reservation machinery's hot atomicState keys from a
write-through in-JVM snapshot, eliminating per-call and
per-continuation-leg DB read chatter.
- Make package deployment asynchronous and request-correlated, with
race-safe recovery for an abandoned scheduled-worker marker, and never
let a worker throw overwrite an already-persisted deploy outcome for the
same request.
- Give the SDK conformance proof the same platform-limiter recovery the
suite uses (one verified watchdog bounce + one retry), and poll the e2e
restore marker through the watchdog so the main app's post-restore
recompile no longer manufactures relay 504s.
- Carry the newest same-rule edit baseline across worker executions (a
JVM mirror beside the manifest, plus a retried file probe), so backup
reuse never depends on cross-execution atomicState visibility and a
transient read cannot unlink a healthy baseline.
- Retain the useful fixes and production `outputSchema` behavior from
#378; live E2E intentionally does not exercise the abandoned
output-schema feature.
- Route regular and official-SDK live MCP coverage through the modern
protocol only.

## Type of change

- [x] `feat` — new feature or capability
- [ ] `fix` — bug fix
- [ ] `chore` — maintenance, dependency bump, or housekeeping
- [ ] `refactor` — code restructure with no behaviour change
- [ ] `docs` — documentation only
- [ ] `test` — tests only
- [ ] `ci` — CI/CD pipeline change

## Changes

- Use standard state-only `input_required` responses and terminal
`complete` tool results for slow writes.
- Bind request state to the exact tool and canonical arguments, prevent
duplicate execution, retain terminal replay, and preserve the configured
all-write concurrency limit; numeric-string `appId`s now reach the
pre-reservation refusal seam the same way numeric ones do.
- Detach indivisible native-app/rule wizard work from relay-bound HTTP
requests.
- Replace the #378 public `opToken`/deployment-job path with an internal
package worker and durable request-specific outcome.
- Concurrent-write accounting is in-memory (`WRITE_REQUEST_LEASES`
static + TTL sweep); the e2e suite runs with the cap off and one
dedicated live test exercises the `too_many_writes_in_flight` refusal
under cap 1.
- `hub_list_files` rejects a non-string `filter` before any hub I/O;
File Manager name filtering is locale-independent.
- Same-rule edit baselines reuse reliably across the detached workers'
executions; the reuse probe retries once before the discard path may
permanently unlink a handle.
- Pin the official Python SDK proof to MCP 2026-07-28 and require one
high-level `Client.call_tool()` to complete a logical write lasting more
than 10 seconds across individually bounded HTTP legs, with
capacity-recovery (bounce + retry on a fresh fixture) when the run lands
on a limiter-exhausted hub.
- Preserve #378's production output-schema definitions and publication
behavior while removing all output-schema live E2E scenarios.
- Addresses #376. Supersedes #378 and #386.

Closes #376.

## Release Notes

- Long Rule Machine and native-app writes now continue automatically
using the current MCP protocol instead of timing out at the cloud relay
boundary.
- Write tools respond noticeably faster over the cloud relay: per-call
bookkeeping no longer performs hub-database round trips on every write.
- Package updates now acknowledge quickly, run in the background, reject
duplicate deployment attempts, and publish a request-correlated final
outcome.
- Concurrent writes remain globally capped to protect the hub from
clients that issue large batches or parallel tool calls.

## Testing

- 331 Python/doctest tests plus the e2e helper suites (115 focused
tests) pass locally.
- Sandbox lint and its self-test pass; Python compilation, Ruff, shell
syntax checks, bundle build, and diff checks pass.
- Groovy 2.4 parsing, Groovy 2.5 Spock, and all four normal/strict
flat/gateway unit-test matrices pass in GitHub Actions.
- Three fully green full-lane live E2E runs on the test hub, including
the official-SDK conformance proof (7/7 scenarios) and the new live
write-cap refusal test. The latest run recorded zero relay 504s and zero
platform-limiter interventions anywhere in the run (from five bounces
and recurring 504s per run at the PR's midpoint), with the full suite
completing in ~55 minutes (from ~69).

## Checklist

- [x] **Unit tests added for any new MCP tools, regressions, or bug
fixes** (required — see [docs/testing.md](docs/testing.md) for the
harness + recipes)
- [x] **e2e tests added for new tools and/or regression tests added for
any bug fix** (see `tests/e2e_test.py`)
- [x] Sandbox lint passes: `python tests/sandbox_lint.py`
- [ ] `./gradlew test` passes locally (or CI confirms)
- [x] Live-hub BAT tests updated if tool behaviour changed (see
`tests/BAT-v2.md`)
- [x] Documentation updated if user-facing behaviour or tool surface
changed
- [x] New/renamed MCP tools follow `AGENTS.md` Tool Design Rules
(naming, annotations, schema) — no tool was added or renamed; changed
contracts follow the same rules.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automatic continuation and replay for long-running writes,
replacing client-supplied operation tokens.
* Added filtered, case-insensitive file listings with pagination
support.
  * Added Rule Machine “stopped” status and expanded health details.
* Added configurable concurrent-write limits and asynchronous package
deployment tracking.
* Added reusable rollback backups with clearer scope and status
reporting.

* **Bug Fixes**
* Improved backup cleanup, validation, error reporting, and
transport-failure recovery.
* Improved handling of missing visual-rule definitions and unset system
status.

* **Documentation**
* Updated backup, continuation, deployment, and SDK conformance
guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:minor Minor version bump on merge (multiple new tools, architectural change)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant