PR #878 landed before this branch and added useRef + deleteButtonRef focus-
management to DetailsTab.tsx. This commit combines that import with the
useId/cloneElement import added here, and preserves the Field component
htmlFor/id wiring from this PR unchanged.
Mechanical conflict resolution by triage-agent; no logic changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The replace directive `=> /plugin` breaks CI builds where go build runs
natively (no /plugin directory). Move the replace to Dockerfile RUN so
it only applies during Docker builds where the plugin is COPYed.
Fixes: "replacement directory /plugin does not exist" on CI runner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Windows Docker Desktop copies host files with CRLF even when
.gitattributes says eol=lf. The entrypoint now strips \r from all
hook .sh/.py files before dropping to agent user. Permanent fix for
the #507 CRLF regression that reappeared after every restart.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds callback_query to AllowedUpdates in Telegram polling. When CEO
clicks Yes/No inline keyboard buttons:
1. Acknowledges press (removes loading spinner)
2. Updates message with 'CEO approved/rejected'
3. Routes 'CEO_DECISION: approve:xyz' as inbound to the agent
Only one workspace polls per bot token (Triage Operator) — other
workspaces with Telegram use outbound-only via direct API.
Fixed: duplicate pollers causing 'terminated by other getUpdates'
errors — removed PM/DevLead/ResearchLead Telegram channel rows
(they send outbound via direct Telegram API calls, not channel manager).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HermesA2AExecutor now supports sending system context as ordered, separate
role=system messages instead of a single concatenated string — the model
format recommended by NousResearch.
Changes:
- HermesA2AExecutor.__init__: new system_blocks kwarg (list[str|None]|None)
stored as an independent copy; None blocks and empty strings silently skipped
- _build_messages(): when system_blocks is not None, emits each non-empty
block as a separate {"role": "system"} entry in Hermes-recommended order
(persona → tools context → reasoning policy); falls through to legacy
system_prompt path when system_blocks is None (backward compatible)
Backward compatibility: existing callers that pass a single system_prompt
string continue to work identically — no changes required.
Tests (12 new, 47 total):
- system_blocks stored as independent copy (mutation safe)
- three-block stacked ordering preserved
- empty / None blocks silently skipped
- all-empty list → zero system messages
- system_blocks overrides system_prompt when both provided
- legacy system_prompt path unchanged
- stacked blocks appear in the live API call kwargs
Closes#499
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Dockerfile COPY for molecule-ai-plugin-github-app-auth was lost
during a rebase earlier this session. Without it, the platform binary
compiled without the TokenProvider interface implementation, causing
/admin/github-installation-token to return 'no token provider registered'.
This forced hourly rolling restarts to refresh GH_TOKEN (the env var
from provision time expires after ~60 min). Each restart also required
re-applying 6 manual patches and caused ~2 min of A2A downtime where
agents reported peers as 'unresponsive'.
With this fix, the gh-wrapper in each container auto-refreshes tokens
via the platform endpoint on every gh call. Zero restarts needed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per UIUX Cycle 5 spec, Dialog.Content should carry an explicit
aria-label="Conversation trace" in addition to the aria-labelledby
automatically wired by Radix Dialog via Dialog.Title. This provides
a fallback accessible name directly on the dialog container element.
All 732 tests pass, build clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
molecule-medo now lives at Molecule-AI/molecule-ai-plugin-molecule-medo
(same pattern as all other plugins). Removed the gitignore exception
that kept it in the monorepo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Corrects the source-input aria-label wording to match the UIUX Cycle 4
spec exactly. Previous commit used "Install plugin from source URL";
spec says "Install from source URL" (matches the visible "Install from
source" section heading). Updates the corresponding test assertions.
No functional change. All 736 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PR#842 merged the docs/opencode.json to main with the correct MCP URL path.
PR#840 branch had an older version — sync to main's content to resolve conflict.
PR#842 merged the docs/opencode.json to main with the correct MCP URL path.
PR#840 branch had an older version — sync to main's content to resolve conflict.
Security Auditor pre-merge conditions for PR#840:
C5: toolCommitMemory passes content directly to DB insert without secret
redaction. Gap is tracked to #838 (platform-wide _redactSecrets pass).
Adds inline TODO(#838) comment at the insert site so the gap is visible
in-code, not only in the issue tracker.
C6: toolDelegateTask sets X-Workspace-ID but no bearer token on the
outbound A2A call. The /workspaces/:id/a2a route is intentionally outside
WorkspaceAuth (by design in router.go). CanCommunicate is enforced before
the request is constructed, and callerID was authenticated by WorkspaceAuth
on the MCP bridge entry point. Documents this trust assumption at the call
site.
The inline JSON example still showed the bare ${MOLECULE_MCP_URL} without
the /workspaces/${WORKSPACE_ID}/mcp path. Updated to match opencode.json fix
in previous commit (bf80f15). Added WORKSPACE_ID to the env section.
Tables: Slack has no table syntax. Converter now detects markdown tables
and renders them as monospace code blocks with aligned columns.
Dividers: replaced unicode em-dash (caused encoding artifacts) with
plain ASCII dashes.
Strikethrough: ~~text~~ converts to ~text~ (Slack native).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The function was defined on a feature branch, referenced by manager.go
and slack_test.go, but never made it to main after the rebase. This
caused go build to fail with 'undefined: FetchChannelHistory', which
Docker masked by using a cached binary from the last successful build.
That cached binary had neither the mrkdwn blocks nor the Level 3
context injection — explaining why Slack messages showed raw markdown
despite the source having the converter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slack's chat.postMessage renders the text field as plain text when
username override is used. Switching to blocks with type=mrkdwn
forces rich formatting (bold, links, code, dividers).
Also restores FetchWorkspaceChannelContext that was lost in rebase.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both were lost during the PR #844 rebase — the converter was in the
source but the binary couldn't compile because FetchWorkspaceChannelContext
was missing from manager.go (interface mismatch). Previous deploys
silently used the cached old binary without the converter.
Also removed unused 'log' import that blocked compilation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agents output standard Markdown (Claude Code default) but Slack uses
its own mrkdwn format. Without conversion:
**bold** shows as literal **bold**
### heading shows as literal ###
[text](url) shows as raw markdown link
Converter handles:
**bold** → *bold* (Slack bold is single asterisk)
### heading → *heading* (bold text, no headings in Slack)
[text](url) → <url|text> (Slack link format)
--- → ——— (visual separator)
`code` and ```blocks``` pass through unchanged
6 new tests: bold, heading, link, hr, code block, mixed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two WCAG violations in the Danger Zone delete flow:
1. WCAG 4.1.3 (Status Messages): the confirmation UI that appears when
the user clicks "Delete Workspace" had no ARIA live region, so screen
readers never announced the confirmation prompt. Adding role="alert"
to the confirmation container makes it an implicit assertive live
region that is announced immediately.
2. WCAG 2.4.3 (Focus Order): pressing Cancel left focus wherever the
browser placed it (often body). Keyboard users had to re-navigate to
find the Delete Workspace button. The Cancel handler now calls
deleteButtonRef.current?.focus() to return focus to the trigger
button, matching the expected modal/disclosure focus-management pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the onboarding card had no landmark role and no
live region, so screen readers had no way to know the card exists or
that the step changed.
- Add role="complementary" aria-label="Onboarding guide" to the card
container so it appears as a named landmark in assistive technology.
- Add a role="status" aria-live="polite" aria-atomic="true" sr-only div
that holds the current step label. When the step state changes React
updates the div content, which the live region broadcasts to the AT
without pulling focus away from the user's current position.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NVDA and other screen readers ignore the title attribute on interactive
elements and non-interactive divs. Add aria-label alongside title on:
- Stop All button (dynamic label reflects active task count)
- Restart All button (dynamic label reflects pending workspace count)
- StatusPill component (online/offline/failed/provisioning counts)
- WsStatusPill component (connected/connecting/disconnected variants)
Inner dot and text spans get aria-hidden="true" so the screen reader
reads the single aria-label rather than individual child nodes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WCAG 1.3.1 / 4.1.3: the error div that appears after a failed workspace
deploy or blank-workspace create had no ARIA live region, so screen
readers never announced it. Adding role="alert" makes the message an
implicit aria-live="assertive" region so assistive technology surfaces
the error immediately without requiring the user to navigate to it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire WCAG 1.3.1 label associations: 6 bare <label>+control pairs in
ConfigTab (Description, Tier, Runtime, Effort, Task Budget, Backend) now
use stable useId() IDs with matching htmlFor/id. Field helper in
DetailsTab updated to generate its own fieldId via useId() and inject it
into the child element via cloneElement, so every Name/Role/Tier field in
edit mode is correctly associated without requiring call-site changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a cron fires, the scheduler now fetches the last 10 messages from
the workspace's Slack channel via conversations.history and prepends them
to the cron prompt as '[Slack channel context — recent team messages]'.
This gives each agent ambient awareness of what peers are doing:
- Backend sees Frontend posted 'PR #840 ready for review' → can check
- Security Auditor sees Backend posted 'new endpoint added' → plans review
- PM sees all engineering activity → better synthesis in rollup
Implementation:
- slack.go: FetchChannelHistory() calls conversations.history, filters
bot's own messages, returns last N as SlackHistoryMessage structs
- manager.go: FetchWorkspaceChannelContext() looks up the workspace's
Slack config, fetches history, formats as readable context block
- scheduler.go: ChannelBroadcaster interface extended with
FetchWorkspaceChannelContext; fireSchedule injects context before
the cron prompt (prepended, not appended, so the agent sees team
context BEFORE its task instructions)
Best-effort: if Slack API fails or workspace has no channels, the
prompt is unchanged. Truncated to 200 chars per message, 10 messages
max to keep prompt overhead bounded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code review findings addressed:
Critical:
1. Bot echo loop: add bot_id + subtype='bot_message' check in ParseWebhook
to prevent outbound auto-posts from triggering inbound → infinite loop
2. Connection leak: close resp.Body immediately after reading instead of
defer inside loop (was holding N connections open for N chunks)
3. Cancelled context: auto-post goroutine now uses context.Background()
with 30s timeout instead of inheriting fireCtx (which gets cancelled
by deferred cancel() when fireSchedule returns)
4. Slug validation: regex ^[a-zA-Z0-9 _-]+$ rejects path traversal and
special chars in [slug] routing
Improvements:
5. Shared HTTP client (slackHTTPClient) for connection pooling instead of
per-request &http.Client{}
6. Rune-safe truncation in BroadcastToWorkspaceChannels for CJK/emoji
7. Log async HandleInbound errors instead of silently discarding
8. url_verification challenge properly returned (c.JSON with challenge)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>