Skip to content

🤖 fix: reject leading-dash workspace names and let a heartbeat go back to the default interval - #5726

Merged
ThomasK33 merged 1 commit into
mainfrom
fix/workspace-name-leading-dash
Oct 6, 2026
Merged

ThomasK33 merged 1 commit into
mainfrom
fix/workspace-name-leading-dash

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes two Settings bugs from the agent bug bash in #5670.

  1. A workspace or branch name that starts with - now fails validation in the form and in the backend. Before the fix, -dash- passed the check, and git worktree add -b -dash- then failed with git's option-parsing error.
  2. A workspace heartbeat can go back to the global default interval. The heartbeat dialog has a "Use the global default (N min)" checkbox under Interval. When it is checked, the workspace saves no interval of its own and follows later changes to the global default. Settings > Heartbeat now shows the valid range (5–1440) for the default interval.

Fixes #5686
Fixes #5692

#5686: leading hyphen

  • Git reads a leading - in git worktree add -b <name> as an option. A - after a / is safe: git worktree add -b feat/-x and -b a-/b both work (checked against git directly). So only the first character is rejected.
  • validateWorkspaceName and validateWorkspaceBranchName reject a leading - with "… names cannot start with a hyphen". The creation form (useWorkspaceName) and the backend create/rename/fork paths use these validators, so both reject the name.
  • I checked other names that pass validation. x-, a-/b, feat/-x and _ all work with git, so I changed nothing else.

#5692: back to the global default interval

intervalMs is now optional in WorkspaceHeartbeatSettingsSchema. An absent value means "use the global default". This PR removes a requirement on an existing field and adds no new persisted field. heartbeat.set takes intervalMs: null to clear the override, and an absent key keeps the saved value (the same pattern as trigger/whenBusy).

Downgrade: an older build reads a record without intervalMs as the global default. normalizeWorkspaceMetadataHeartbeat in src/node/config/index.ts (unchanged) falls back to heartbeatDefaultIntervalMs. The new test reads workspace metadata after a global change and gets the new default. Older builds load and save such a record normally, because they do not parse workspace entries through the zod schema. One exception: an older build's mux_config_write agent tool validates the whole file against the old schema and refuses to write while such a record exists. Tracked in #5742 (workaround: set an interval in the dialog).

Every reader of a workspace heartbeat intervalMs, and how it handles an absent value:

Reader Handling
src/node/config/index.ts normalizeWorkspaceMetadataHeartbeat (workspace metadata) Unchanged. It already falls back to the global default.
heartbeatService.getSanitizedTrackingIntervalMs (scheduling: activity events, metadata events, config resync on every tick) Unchanged. It already uses raw ?? heartbeatDefaultIntervalMs ?? built-in. A global change applies on the next tick.
workspaceService heartbeat executor (sanitizeHeartbeatIntervalMs) Unchanged. It already falls back.
workspaceService.getHeartbeatSettings Returns the effective interval by default (the heartbeat tool uses this). heartbeat.get IPC passes resolveDefaultInterval: false and gets the saved shape.
workspaceService.setHeartbeatSettings Persists the sparse shape. null clears the override, and an absent key preserves it. The scheduleUpdatedAt stamp compares effective intervals. Returns the effective interval.
Heartbeat tool (tools/heartbeat.ts) and summarizeHeartbeatSettings Get the effective interval. The summary text falls back to "the global default interval".
HeartbeatToolCall card Falls back to "Enabled" / "global default" if an old result lacks the field.
useWorkspaceHeartbeat and WorkspaceHeartbeatModal Read the sparse shape, show the checkbox, and save null for the default.
/heartbeat off (chatCommands.ts) Now omits intervalMs. Before, it wrote the built-in default, which silently pinned a value.
Debug CLI (src/cli) Reads no heartbeat interval.

Behavior change: a heartbeat created without an interval (dialog default, /heartbeat off, or the agent tool without intervalMs) now follows the global default. Before, it saved a copy of the default at creation time. A dialog save with the checkbox unchecked pins the shown value as an override, as before.

Known limit: a global default change does not stamp scheduleUpdatedAt. A fixed-interval (trigger: "interval") workspace that follows the default re-anchors on the next tick, but the restart-anchoring rule uses the older stamp. I did not add a persisted field for this. Tracked in #5741.

No new operation: the checkbox is a form control (Tab and Space), and Save stays the operation. So I added no keyboard shortcut.

Validation

Pre-fix failures on origin/main (tests written first):

(fail) validateWorkspaceName > invalid names > rejects a leading hyphen
(fail) validateWorkspaceBranchName > rejects -dash-
(fail) validateWorkspaceBranchName > rejects -a/b
(fail) WorkspaceService heartbeat settings > null clears the interval override so the workspace follows the global default (#5692)
    Expected: true  Received: false
TestingLibraryElementError: Unable to find role="checkbox" and name `/Use the global default \(15 min\)/`   (2 modal tests)

After the fix, make static-check passes, along with 691 tests in 16 related files (on the rebased head) (validation, heartbeat settings/service/tool, modal, hook, Settings section, chat commands, config, workspace/task service).

One existing test changed on purpose: chatCommands.test.ts expected /heartbeat off to write the built-in default interval. It now expects no interval.

Screenshots (Storybook Components/WorkspaceHeartbeatModal/LongMessageDesktop, a workspace with a 7-minute override):

Before, 1200px: no way back to the default.
Before

After, 1200px, with "Use the global default" checked:
After

After, 390px:
After at 390px

Final check

A clean-context reviewer checked head 668197d12e against the criteria and recommended "ready with tracked follow-ups". Follow-ups: #5741 (re-anchoring after a global default change) and #5742 (downgrade note for mux_config_write). Its third note is cosmetic: the disabled interval field keeps showing an old default if the global default changes while the dialog is open. I did not file it.

Rebase: main gained #5717, which conflicted in the heartbeat hook and modal. The rebased head 6557b3828b keeps #5717's rule: when the config cannot load, globalDefaultIntervalMs stays undefined, and the checkbox label leaves out the number. The normal and security reviews on 6557b3828b were clean. A second clean-context final check on 6557b3828b recommended "ready with tracked follow-ups" and named one more: #5745 (the disabled field shows the built-in value when the default is unknown).

Review assessments used: 6 of 6 (normal and security reviews on 668197d12e and on 6557b3828b, and the two final checks).

Risks

Medium-low. The persisted shape changes from "always has intervalMs" to "intervalMs optional". All readers already had a fallback (see the table), and the downgrade read path is tested. The modal unit tests save the shown interval without typing, because happy-dom does not fire onChange for this number input. Typing is pre-existing behavior.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-06T13:05:46.877396Z 6557b38 New commits
🔒 Security Review ✅ Completed 2026-10-06T13:08:04.981579Z 6557b38 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ThomasK33

Copy link
Copy Markdown
Member Author

Readiness record for 6557b3828b:


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

@ThomasK33
ThomasK33 added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit f08f1fb Oct 6, 2026
42 of 43 checks passed
@ThomasK33
ThomasK33 deleted the fix/workspace-name-leading-dash branch October 6, 2026 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant