| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix: recreate the resume terminal only after spawnPty accepts the replacement (#575) restartWithModel's resume path ran disposeTerminal+acquireTerminal BEFORE the spawnPty call that can still fail, so a refused spawn (!res.ok, e.g. cwd gone or CLI missing) or a refused resume (res.resumed !== true, the main process's requireResume early-return) threw after the destroy: the pool kept the fresh empty xterm, the old one's scrollback was unrecoverable (node-pty keeps none), and the agent's action label stayed stuck at 'recreating terminal…' — one failed Restart & Continue erased the pane the button exists to redraw. The fix reorders: kill/reset stay pre-spawn, but the destroy/recreate/ generation-bump now runs after the two throw gates, only when the replacement process was actually accepted. The spawn answer beats the CLI's first frame, so startup output cannot be missed, and the remount's attach re-requests a PTY redraw for anything it raced. The Electron repro (test/repro/bug-14.*) becomes a proper regression test, test/restart-terminal-preserve.test.cjs: the failed-restart scenarios must keep the pooled entry, scrollback and label, while a successful restart must still recreate the terminal — only after the spawn answer. | 1 个月前 | |
Merge pull request #573 from TTAWDTT/fix/bug-12-integrations-security fix: add socket timeouts to postSlackReply and downloadSlackFile | 25 天前 | |
fix(hive): an agent killed by a signal no longer dies without a trace Observed live 2026-08-24: Michael's `claude` CLI panicked 923ms into startup (Bun, SIGILL) and the hive recorded exactly {"kind":"archive","agentId":"god","archived":true} No exit code, no signal, no output. The crash banner existed only in a UI terminal pane -- nowhere on disk, not under harnessHome and not in userData. A two-second death and a completed agent were indistinguishable in the only durable record the hive keeps, so anything reading log.jsonl to ask "is the floor healthy" saw a clean spawn/archive pair. Root cause was upstream of the recording. pty.ts had the answer in hand and threw half of it away: proc.onExit(({ exitCode, signal }) => { this.safeSend(`pty:exit:${id}`, { exitCode, signal }, owner); // renderer gets both this.exitHandler?.(id, exitCode); // main gets only the code A process killed by SIGILL/SIGSEGV/SIGKILL exits with code 0 and a non-zero signal, so every consumer of exitHandler was blind to signal deaths BY CONSTRUCTION -- no amount of care downstream could have seen them. Changes: * pty.ts keeps a bounded 8KB rolling tail of each session's output and passes {signal, tail, command, cwd} to the exit handler as PtyExitInfo. By value, deliberately: onExit deletes the session from the map BEFORE calling the handler, so a handler that tried to look the command up would always get undefined. (I wrote that bug first, then found it.) * hive.recordAgentExit() writes the death down, and SPLITS it: - log.jsonl gets structured, non-sensitive fields (exitCode, signal, abnormal, tailPath); - crashes/ gets the raw tail, and is added to the hive's .gitignore. That split is the load-bearing part. The hive is a git repo committed on every change, and the tail is whatever the provider printed -- tokens, paths, prompt fragments. Writing it into log.jsonl would make a leaked secret permanent. A test asserts a fake key reaches the dump and never the log. * A clean exit records NOTHING. This is a diagnostic, not an audit log. * index.ts records before teardownPty, because teardown drops the pty->agent mapping and after it runs we can no longer say whose process died. Tests: 8 new, and both files were shown RED against the baseline sources before being trusted -- 5/5 and 3/3 failing with the original hive.ts / pty.ts checked out, green after restoring the patch. The e2e test drives the real PtyManager against a process that prints a panic banner and then `kill -ILL $$`, which is the exact shape of the crash that motivated this. Suite: 552 -> 560 tests, 551 -> 559 passing. The single remaining failure is the pre-existing hive-hook-node harness race (its run() helper writes to a child's stdin with no 'error' listener), unrelated and untouched here. Upstream-shaped on purpose: no ASA-specific naming, no dependency on anything outside this repo, and the split-and-gitignore design is defensible for any user. This is a candidate to send to chaitanyagiri/munder-difflin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vc9zjyDBnn8Y5LaEjYEZqi | 2 个月前 | |
fix(hive): an agent killed by a signal no longer dies without a trace Observed live 2026-08-24: Michael's `claude` CLI panicked 923ms into startup (Bun, SIGILL) and the hive recorded exactly {"kind":"archive","agentId":"god","archived":true} No exit code, no signal, no output. The crash banner existed only in a UI terminal pane -- nowhere on disk, not under harnessHome and not in userData. A two-second death and a completed agent were indistinguishable in the only durable record the hive keeps, so anything reading log.jsonl to ask "is the floor healthy" saw a clean spawn/archive pair. Root cause was upstream of the recording. pty.ts had the answer in hand and threw half of it away: proc.onExit(({ exitCode, signal }) => { this.safeSend(`pty:exit:${id}`, { exitCode, signal }, owner); // renderer gets both this.exitHandler?.(id, exitCode); // main gets only the code A process killed by SIGILL/SIGSEGV/SIGKILL exits with code 0 and a non-zero signal, so every consumer of exitHandler was blind to signal deaths BY CONSTRUCTION -- no amount of care downstream could have seen them. Changes: * pty.ts keeps a bounded 8KB rolling tail of each session's output and passes {signal, tail, command, cwd} to the exit handler as PtyExitInfo. By value, deliberately: onExit deletes the session from the map BEFORE calling the handler, so a handler that tried to look the command up would always get undefined. (I wrote that bug first, then found it.) * hive.recordAgentExit() writes the death down, and SPLITS it: - log.jsonl gets structured, non-sensitive fields (exitCode, signal, abnormal, tailPath); - crashes/ gets the raw tail, and is added to the hive's .gitignore. That split is the load-bearing part. The hive is a git repo committed on every change, and the tail is whatever the provider printed -- tokens, paths, prompt fragments. Writing it into log.jsonl would make a leaked secret permanent. A test asserts a fake key reaches the dump and never the log. * A clean exit records NOTHING. This is a diagnostic, not an audit log. * index.ts records before teardownPty, because teardown drops the pty->agent mapping and after it runs we can no longer say whose process died. Tests: 8 new, and both files were shown RED against the baseline sources before being trusted -- 5/5 and 3/3 failing with the original hive.ts / pty.ts checked out, green after restoring the patch. The e2e test drives the real PtyManager against a process that prints a panic banner and then `kill -ILL $$`, which is the exact shape of the crash that motivated this. Suite: 552 -> 560 tests, 551 -> 559 passing. The single remaining failure is the pre-existing hive-hook-node harness race (its run() helper writes to a child's stdin with no 'error' listener), unrelated and untouched here. Upstream-shaped on purpose: no ASA-specific naming, no dependency on anything outside this repo, and the split-and-gitignore design is defensible for any user. This is a candidate to send to chaitanyagiri/munder-difflin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vc9zjyDBnn8Y5LaEjYEZqi | 2 个月前 | |
fix(provider): default Cursor CLI binary to cursor-agent Use cursor-agent as the canonical spawn command (official installer name) and treat agent as an alias in inferAgentProvider, per upstream review. Co-authored-by: Cursor <cursoragent@cursor.com> | 2 个月前 | |
fix(hive): keep hire role out of live status captions Roster description is the durable job string, same as registry.role. Respawn was able to write idle captions like "on standby" into identity.md and wipe hive capabilities; treat those as status, not a role. Co-authored-by: Cursor <cursoragent@cursor.com> | 2 个月前 | |
fix(hires): clear cancelled batches and update caps atomically Why: - Cancelling a batch review left unseen manifests queued, so stale hires and progress resurfaced when the modal reopened. - Command Center replaced the whole token-cap map from a renderer snapshot and could erase caps written by the hire flow. What: - Clear the review queue on modal close and reset later imports to a fresh run. - Extend the main-process per-agent cap IPC to atomically set or clear one entry. - Route Command Center cap edits through the atomic IPC and reconcile optimistic state. - Share the cap ceiling constant and remove duplicate manifest application. - Add RED-to-GREEN regressions for cancel/reset, atomic set/clear, and renderer wiring. Risk: - Cap persistence remains best-effort in the renderer; failed optimistic edits are reconciled from persisted config. - No IPC channel name or existing numeric set call changes. Tests: - node --test test/hire-queue.test.cjs test/agent-token-cap.test.cjs test/hire-import.test.cjs (17 passed) - npm run test:focused (256 passed) - node --test test/*.test.cjs (275 passed) - npm run typecheck - npm run build - git diff --check Live Docs: - Not applicable; this completes review feedback on the pending batch-hire PR. (cherry picked from commit 80b9867ba6ec3624516baec27c501699ef0f719d) | 2 个月前 | |
fix(terminal): carry a partial ANSI escape across pty chunks stripAnsi was stateless, so an escape split by a pty read boundary lost its head and rendered the tail as literal text in the thought bubble. Add createAnsiStripper, a per terminal stripper that holds the unfinished tail and prepends it to the next chunk. The carry is capped at 256 bytes, past that a never completed escape is flushed through the stateless stripper so a lone ESC cannot buffer forever. Unit tests cover SGR, CUF, OSC, charset and two byte splits plus the cap. | 2 个月前 | |
feat(i18n): land the Arabic UI half of PR #213, gated on the chosen language @abo123v-glitch's PR was split in 59d721ed: the terminal half landed, the UI half was held waiting for an Arabic reader. The founder overrode that hold — English stays the default, coverage and accessibility beat correctness, and partial Arabic that falls back to English is fine. This is that half, plus his follow-up that picking Arabic must turn terminal RTL on by itself. WHAT IS NEW i18n/locales/ar.json all 1197 keys — full coverage, no fallbacks used i18n/useDirection.ts useRtl() + useDirectionSync(): the ONE gate markdown/rehypeAutoDir.ts the contributor's plugin, unchanged, now gated terminal/useArabicTerminalSync.ts pushes a language switch to open terminals global.css a [dir="rtl"] section, inert for everyone else 17 dir sites across 12 components, each behind the gate THE GATE IS ONE FUNCTION, ON PURPOSE Every RTL behaviour reads `isRtlLanguage(i18n.language)` and nothing else — not navigator, not the OS, not the text on screen. A scattered `language === 'ar'` check is one somebody eventually forgets to write, and each miss is a layout change for an English user. `dir` goes on <html> rather than a React wrapper because modals and the fullscreen terminal portal outside the tree. FOUR CHANGES TO WHAT THE PR PROPOSED The PR re-added the Google Fonts <link> that 41ea4c37 deliberately removed, and put IBM Plex Sans Arabic in all three token stacks. That is a boot-time network fetch, blocked in mainland China, and an ungated type change for every user. Dropped: the bundled stacks already name Geeza Pro and Noto Naskh Arabic, and the built CSS still fetches no font. rehypeAutoDir sets direction from CONTENT, so ungated it fires for an English user the moment an agent quotes a line of Arabic. Kevin held it for exactly this. It now runs only for an RTL app language, which is the design decision he said was missing — Arabic ships, so the key is the selected locale. xterm is pinned to `direction: ltr` under an RTL page. `direction` inherits, and without this the viewport picks up rtl from <html> and right-aligns the whole cell grid — reversing box drawing while the pty still counts columns from the left. Per-row direction is a different layer and already has .cth-bidi. The two Kanban Q&A dir sites were dropped as superseded: that trail now renders through MarkdownPreview, which is gated one level down. THE TERMINAL TOGGLE NOW FOLLOWS THE LANGUAGE It was an artifact of build order — it shipped when Arabic could not be selected at all, so a manual switch was the only door. A user who picks Arabic has already answered the question it asks. It is a DEFAULT, not a derived value: the state is three-valued (unset / forced on / forced off), because an English user whose logs carry Arabic has a real reason to switch it on, and an Arabic user may want the GPU renderer's speed back. An explicit choice survives a language switch in both directions. ALREADY-OPEN TERMINALS ARE UPGRADED IN PLACE, NOT REBUILT The office scene can be rebuilt on a language switch (a8292697) because it is derived from state we still hold. A terminal cannot: its scrollback lives only in xterm's buffer and the pty will not resend it, so recreating one would eat the user's history. Dropping the WebGL lease is enough — releaseWebglRenderer already hands painting to the DOM renderer with buffer, scrollback and pty subscription intact, and the DOM renderer is what Arabic mode needs anyway. Every step of enableArabicRendering has a matching undo, which is what makes the switch live in both directions. WHAT IS NOT VERIFIED, AND IS NOT CLAIMED Whether the Arabic READS well. The strings were written by an agent, not reviewed by an Arabic reader. The tests cover coverage, shape, interpolation and inertness; none of them says the translation is correct. Do not read a green run as a review. Suite 709/709 (693 before). Gates verified by mutation: unscoping the RTL CSS, un-pinning any of the four xterm selectors, ungating the markdown plugin or a component dir site, making isRtlLanguage a prefix guess, restoring the OS locale sniff, or removing the joiner undo each fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJRooxvQEZXRyyee79cmPu | 1 个月前 | |
feat(i18n): land the Arabic UI half of PR #213, gated on the chosen language @abo123v-glitch's PR was split in 59d721ed: the terminal half landed, the UI half was held waiting for an Arabic reader. The founder overrode that hold — English stays the default, coverage and accessibility beat correctness, and partial Arabic that falls back to English is fine. This is that half, plus his follow-up that picking Arabic must turn terminal RTL on by itself. WHAT IS NEW i18n/locales/ar.json all 1197 keys — full coverage, no fallbacks used i18n/useDirection.ts useRtl() + useDirectionSync(): the ONE gate markdown/rehypeAutoDir.ts the contributor's plugin, unchanged, now gated terminal/useArabicTerminalSync.ts pushes a language switch to open terminals global.css a [dir="rtl"] section, inert for everyone else 17 dir sites across 12 components, each behind the gate THE GATE IS ONE FUNCTION, ON PURPOSE Every RTL behaviour reads `isRtlLanguage(i18n.language)` and nothing else — not navigator, not the OS, not the text on screen. A scattered `language === 'ar'` check is one somebody eventually forgets to write, and each miss is a layout change for an English user. `dir` goes on <html> rather than a React wrapper because modals and the fullscreen terminal portal outside the tree. FOUR CHANGES TO WHAT THE PR PROPOSED The PR re-added the Google Fonts <link> that 41ea4c37 deliberately removed, and put IBM Plex Sans Arabic in all three token stacks. That is a boot-time network fetch, blocked in mainland China, and an ungated type change for every user. Dropped: the bundled stacks already name Geeza Pro and Noto Naskh Arabic, and the built CSS still fetches no font. rehypeAutoDir sets direction from CONTENT, so ungated it fires for an English user the moment an agent quotes a line of Arabic. Kevin held it for exactly this. It now runs only for an RTL app language, which is the design decision he said was missing — Arabic ships, so the key is the selected locale. xterm is pinned to `direction: ltr` under an RTL page. `direction` inherits, and without this the viewport picks up rtl from <html> and right-aligns the whole cell grid — reversing box drawing while the pty still counts columns from the left. Per-row direction is a different layer and already has .cth-bidi. The two Kanban Q&A dir sites were dropped as superseded: that trail now renders through MarkdownPreview, which is gated one level down. THE TERMINAL TOGGLE NOW FOLLOWS THE LANGUAGE It was an artifact of build order — it shipped when Arabic could not be selected at all, so a manual switch was the only door. A user who picks Arabic has already answered the question it asks. It is a DEFAULT, not a derived value: the state is three-valued (unset / forced on / forced off), because an English user whose logs carry Arabic has a real reason to switch it on, and an Arabic user may want the GPU renderer's speed back. An explicit choice survives a language switch in both directions. ALREADY-OPEN TERMINALS ARE UPGRADED IN PLACE, NOT REBUILT The office scene can be rebuilt on a language switch (a8292697) because it is derived from state we still hold. A terminal cannot: its scrollback lives only in xterm's buffer and the pty will not resend it, so recreating one would eat the user's history. Dropping the WebGL lease is enough — releaseWebglRenderer already hands painting to the DOM renderer with buffer, scrollback and pty subscription intact, and the DOM renderer is what Arabic mode needs anyway. Every step of enableArabicRendering has a matching undo, which is what makes the switch live in both directions. WHAT IS NOT VERIFIED, AND IS NOT CLAIMED Whether the Arabic READS well. The strings were written by an agent, not reviewed by an Arabic reader. The tests cover coverage, shape, interpolation and inertness; none of them says the translation is correct. Do not read a green run as a review. Suite 709/709 (693 before). Gates verified by mutation: unscoping the RTL CSS, un-pinning any of the four xterm selectors, ungating the markdown plugin or a component dir site, making isRtlLanguage a prefix guess, restoring the OS locale sniff, or removing the joiner undo each fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJRooxvQEZXRyyee79cmPu | 1 个月前 | |
fix(0.4.7-rc): ask-me ordering, release drop for fresh installs, compact-latch tests Three specified items for the 0.4.7-rc round, cut from release/0.4.6-rc so what runs under `npm run dev` is rc1-plus-these and nothing else. 1. ASK ME board ordering (askme-board-unordered) The board had NO comparator: `tasks.filter(waitsOnHuman)` rendered in whatever order cards happened to sit in a 440KB tasks.json, so a question from five minutes ago could land below one from three days ago. Sorts the OUTER card list by each card's newest still-open ask, descending. "Open" is derived from openQuestion() — the same predicate waitsOnHuman already calls — rather than defined a second time. Unparseable/missing askedAt sorts last and never throws. The humanQA array INSIDE a card is a conversation history rendered through the "VIEW N EARLIER ANSWERS" collapse and is left strictly chronological; a test asserts nothing reverses it. The ordering rule lives in a new import-free askMeOrder.ts so the comparator gets a real behavioural test; AskMeTab.tsx applies it. Testing it inside the .tsx would only have been possible as a source-string assertion, which cannot catch an inverted comparator. 2. Release drop reaches fresh installs (release-drop-reaches-everyone) VERIFIED, NOT REIMPLEMENTED: the 0.4.6 -> 0.4.7 half already works, because the stamp write is unconditional while the emit is gated, so any install that boots 0.4.6 once is armed. Untouched. The real defect: the drop required a `last-run-version` stamp to already exist, but the stamp and its reader shipped in the SAME release, so no earlier install had the file and a fresh install had nothing either. New pure shouldShowReleaseDrop(previous, current) in shared/updateState.ts: (a) fresh install -> fires (b) 0.4.6 -> 0.4.7 -> fires once (c) same version -> never (d) 0.4.7-rc.1 -> rc.2 -> fires, deliberately downgrade -> never (e) UpdateToast drop-block behaviour unchanged (d) is the isNewer edge, FIXED LOCALLY rather than declined: isNewer discards -rc.N, so the decision asks "is this a downgrade?" instead of "is this newer?". Doing it in the drop decision keeps prerelease semantics off the badge/pending state machine, which reads isNewer for other decisions and is frozen 0.4.6 behaviour under active rehearsal. Durability: the stamp write gets its own try/catch and the drop fires only when the stamp actually LANDED. An unwritable userData otherwise leaves `previous` null forever, which would reopen the drop on every single boot. 3. Compaction-latch tests (compact-latch-untested) #270's code is correct and is NOT refactored or touched. queueDelivery.ts has zero imports by design, so both primitives the latch rests on get real behavioural tests: canDeliverToAgent refuses an agent blocked on a human prompt, and deliverWithAcknowledgement reports false without acknowledging on a failed send. The two guards inside useHive.ts (the pre-enqueue gate, and the latch written only when `sent`) are pinned by source assertions, the house pattern for a hook whose import graph cannot load under node:test. All three guards were mutation-tested: inverting the comparator, restoring the old `previous &&` requirement, and dropping the `sent &&` latch gate each fail their tests. Full suite: 616 tests, 611 pass in-sandbox; the 4 failures and 1 timeout are all EPERM-on-$HOME / Electron-binary sandbox artifacts and pass 26/26 when re-run outside the sandbox. typecheck node + web clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
security: keep the OS sandbox on in auto mode Auto mode dropped every sandbox because a hive worker writes to its agent folder under <harnessHome>/hive/agents/<id>/, outside the project cwd. That is a path problem, not a reason to run unsandboxed. - codex: -a never -s workspace-write instead of the full bypass; hive.ts adds the agent dir, hive root and palace via --add-dir. An explicit posture on the command line (including the old bypass flag) still wins. - claude: the per-session settings.json now enables sandbox.enabled with filesystem.allowWrite plus permissions.additionalDirectories for the same dirs. bypassPermissions only silences prompts; verified live that the sandbox still denies writes outside cwd and the listed dirs. - failIfUnavailable stays off so platforms without a sandbox spawn as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5X7vQmQDwrTCn2qBzuBiJ | 2 个月前 | |
fix the 425 test-file resolution: keep both test blocks intact The conflict region overlaps a test body, so resolving it by taking whole sides truncates the floor-budget test and the file stops parsing. Append 425's block before the final process.exit instead. | 1 个月前 | |
fix(hive): let a broadcast reach agents on hookless providers Fan-out gated on canReceiveInbox, so an agent running the `custom` provider never heard a `to: 'broadcast'` message — while a DIRECT message to that same agent was delivered fine, as a terminal work order. deliver() already handles a hookless target: it tries emitTerminalHandoff and only bounces to the god when the renderer is unavailable. The extra gate on the fan-out list was therefore redundant for the providers it allowed, and lossy for the ones it excluded. Fan-out now selects the agents direct mail can reach (skipping the sender, the send-only prep assistant and archived agents), and the existing per-target path decides how each one is served. Trade-off: with the renderer down, a broadcast to N hookless agents produces N bounces to the god instead of silence — the same thing N direct messages already produce, and the loud failure rather than the quiet one. Extracted the rule into src/shared/broadcast.ts so it is testable on its own, following queueDelivery / codexRemote. | 2 个月前 | |
feat(i18n): bundle the app's three faces, drop Google Fonts, add CJK/Arabic fallbacks index.html loaded Press Start 2P, Inter and JetBrains Mono from fonts.googleapis.com on every boot. Both that host and fonts.gstatic.com are blocked in mainland China, so the app started there with all three faces failing: the Press Start 2P brand chrome degraded to generic monospace and the whole UI looked broken, for exactly the users we are adding a zh-CN locale for. A desktop app should not need the network to render its own typography. Bundle the latin subsets instead. Inter and JetBrains Mono are variable, so one file each covers weights 400 to 700, and the three files together are 84KB. Verified in a real build: they emit as hashed assets, the @font-face URLs resolve locally, and nothing in the output fetches a font. No CJK face is bundled. A full one is 8-10MB and every platform we ship on already carries a good one, so the token stacks name the system faces instead (PingFang SC, Hiragino Sans GB, Microsoft YaHei, Noto Sans CJK SC) plus Geeza Pro and Noto Naskh Arabic. They are named explicitly rather than left to `system-ui`, because system-ui resolves to Segoe UI on Windows, which has no CJK coverage at all. Also tightened: style-src and font-src no longer allow either Google host, and the two preconnects are gone. Three things worth knowing: * tokens.css and tokens.ts both define these stacks and say "update both together", with nothing enforcing it. A test now compares them. * The release-drop iframe carries its own font stack in shared/releaseDrop.ts and got the same fallbacks. It still <link>s Geist from Google. Left alone deliberately: Geist is not bundled, and removing it changes designed typography for every user, not just blocked ones. Flagged for the founder. * The fonts are OFL 1.1 and the license requires the notice to travel with them, so src/renderer/src/assets/fonts/LICENSE.txt ships alongside. Verified by mutation: re-opening the CSP fails 1 of 9, reverting the tokens.ts stack fails 2 of 9, and dropping the fonts.css import fails 1 of 9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 1 个月前 | |
fix(windows): write the folder-trust key under the path Claude Code reads (#607) `ensureClaudeProjectTrust` writes `projects[cwd].hasTrustDialogAccepted` into `~/.claude.json` using the raw cwd. On Windows that is a backslash path, but Claude Code looks the entry up under a path normalised to FORWARD slashes - its own lookup walks parents with `o.startsWith(r + "/")`, and the entries it writes on Windows are keyed "C:/Users/...". So the flag lands where Claude never reads. The folder therefore stays untrusted and the spawned agent hits the interactive "Accessing workspace / Quick safety check" dialog it cannot answer. That dialog defaults to "No, exit", so the orientation prompt's own Enter selects it: claude exits 1, god never boots, and every message to it sits at "waiting". Both spellings are now written. On macOS and Linux the two strings are identical, the Set collapses to a single key, and behaviour is exactly what it is today - so this needs no platform check. Verified on Windows with Claude Code 2.1.263 by reproducing the spawn through this app's own node-pty: an untrusted folder shows the dialog, the repaired folder boots straight to the prompt. A worker whose cwd already had a forward-slash key from ordinary CLI use survived the same launch, which is what isolated the cause. The four existing expectations in claude-permissions-config.test.cjs are now platform-aware - they collapse to exactly today's single-key assertions on POSIX - and a regression test covers a config trusted only under the raw spelling. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
test(windows): make platform fixtures deterministic (#403) | 1 个月前 | |
test(windows): make platform fixtures deterministic (#403) | 1 个月前 | |
harden(spawn): only resolve plain command names against the shell PATH The engine-command resolver took a bare command token and looked it up through an interactive login shell (`which <token>`) and, on Windows, `where` under `shell:true`. Constrain that token to a plain command name (`[A-Za-z0-9._+-]`) or an absolute path before it reaches any shell, and drop `shell:true` from the Windows lookup so the argument is never re-parsed. Two independent copies of the resolver (`pty.ts` and `shellEnv.ts`) get the same guard, and the spawn-request intake in `index.ts` refuses a bin that is neither a plain name nor an absolute path before any resolution runs. Absolute paths pass through unchanged, since node-pty execs them directly. test/command-name-validation.test.cjs pins the predicate and that each of the three sites actually calls it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LDbmAwrgtGskFGCRkYZBx4 | 1 个月前 | |
fix(git): render the commit history ourselves so it fits the panel The history was unreadable at any panel width — commit rows piled on top of each other, and the subject was missing entirely. The cause was `commit-graph`, which cannot be made to fit a resizable sidebar. It positions every row absolutely at a hardcoded `height: 4rem` INDEPENDENT of the `commitSpacing` it draws the graph at, so our compact spacing stacked rows 36px deep. It reserves `max-width: 500px` for the SVG regardless of available width, so in a narrow panel the text is squeezed into the remainder, wraps, and collides again — which is why a CSS pass fixed the wide case and not the narrow one. Its class names carry a build hash, so corrective CSS is substring-matched and breaks on a version bump. And it never rendered the commit SUBJECT at all: the log read `sha · author · time`, omitting the one field a history is read for. `components/git/graph.ts` already had a lane algorithm and themed lane colours, written for this and sitting unused. It is now the render path: a pointer-transparent SVG rail sized from the lanes actually in use, plus one flex row per commit at a fixed height — hash, subject, ref chip, relative time, with the subject taking the leftover width and being the only element allowed to ellipse. Readable from a 240px sidebar to a full-width panel. Also removes the `.cth-commit-graph * { font-family: … !important }` blanket that existed to reach inside the widget. `!important` beats inline styles, so it would have silently killed the monospace on hashes and timestamps in the new markup. Alongside: - The IDE's git rail collapses, so the file tree can have the height back. Only the pane folds; the CHANGES/HISTORY/COMPARE strip stays visible so the control that reopens it is always reachable. Persisted. - Two scrollers that never scrolled: `overflow: auto` inside a `maxHeight` column parent sizes to its CONTENT without `flex: 1`, so it overflows the cap instead of ever reaching its own scroll threshold. Fixed for the CHANGES list and the per-commit file list. - The fullscreen roster collapses too, for a full-width terminal. Persisted. layoutGraph was untested while unused and is load-bearing now: 7 cases covering lane reuse, merge forks, out-of-window parents, and themed colours. Typecheck clean (node + web), 187/187 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(0.4.7-rc): ask-me ordering, release drop for fresh installs, compact-latch tests Three specified items for the 0.4.7-rc round, cut from release/0.4.6-rc so what runs under `npm run dev` is rc1-plus-these and nothing else. 1. ASK ME board ordering (askme-board-unordered) The board had NO comparator: `tasks.filter(waitsOnHuman)` rendered in whatever order cards happened to sit in a 440KB tasks.json, so a question from five minutes ago could land below one from three days ago. Sorts the OUTER card list by each card's newest still-open ask, descending. "Open" is derived from openQuestion() — the same predicate waitsOnHuman already calls — rather than defined a second time. Unparseable/missing askedAt sorts last and never throws. The humanQA array INSIDE a card is a conversation history rendered through the "VIEW N EARLIER ANSWERS" collapse and is left strictly chronological; a test asserts nothing reverses it. The ordering rule lives in a new import-free askMeOrder.ts so the comparator gets a real behavioural test; AskMeTab.tsx applies it. Testing it inside the .tsx would only have been possible as a source-string assertion, which cannot catch an inverted comparator. 2. Release drop reaches fresh installs (release-drop-reaches-everyone) VERIFIED, NOT REIMPLEMENTED: the 0.4.6 -> 0.4.7 half already works, because the stamp write is unconditional while the emit is gated, so any install that boots 0.4.6 once is armed. Untouched. The real defect: the drop required a `last-run-version` stamp to already exist, but the stamp and its reader shipped in the SAME release, so no earlier install had the file and a fresh install had nothing either. New pure shouldShowReleaseDrop(previous, current) in shared/updateState.ts: (a) fresh install -> fires (b) 0.4.6 -> 0.4.7 -> fires once (c) same version -> never (d) 0.4.7-rc.1 -> rc.2 -> fires, deliberately downgrade -> never (e) UpdateToast drop-block behaviour unchanged (d) is the isNewer edge, FIXED LOCALLY rather than declined: isNewer discards -rc.N, so the decision asks "is this a downgrade?" instead of "is this newer?". Doing it in the drop decision keeps prerelease semantics off the badge/pending state machine, which reads isNewer for other decisions and is frozen 0.4.6 behaviour under active rehearsal. Durability: the stamp write gets its own try/catch and the drop fires only when the stamp actually LANDED. An unwritable userData otherwise leaves `previous` null forever, which would reopen the drop on every single boot. 3. Compaction-latch tests (compact-latch-untested) #270's code is correct and is NOT refactored or touched. queueDelivery.ts has zero imports by design, so both primitives the latch rests on get real behavioural tests: canDeliverToAgent refuses an agent blocked on a human prompt, and deliverWithAcknowledgement reports false without acknowledging on a failed send. The two guards inside useHive.ts (the pre-enqueue gate, and the latch written only when `sent`) are pinned by source assertions, the house pattern for a hook whose import graph cannot load under node:test. All three guards were mutation-tested: inverting the comparator, restoring the old `previous &&` requirement, and dropping the `sent &&` latch gate each fail their tests. Full suite: 616 tests, 611 pass in-sandbox; the 4 failures and 1 timeout are all EPERM-on-$HOME / Electron-binary sandbox artifacts and pass 26/26 when re-run outside the sandbox. typecheck node + web clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
docs(config): trim the developer notes to house style The notes added with this change ran long, explained mechanics the code already shows, and cited an issue number a future reader may not be able to look up. Rewritten to state the intent at a level anyone can follow, and to carry their own reason rather than pointing at a tracker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix(control): cap the per-agent steer queue so a stalled agent cannot accumulate unbounded notes steer() pushes a note onto the queue for the next hook boundary, and takeSteer() drains one per hook. A halted/stalled agent never drains, so a burst of steers (closing-time loop, a stuck caller, repeated operator input) grows the queue in memory without bound. Cap it at MAX_PENDING_STEERS and drop the OLDEST note when full so the most recent instruction still lands on the next boundary. | 2 个月前 | |
fix(cost): report lifetime spend, not spend since the last app restart fleet.json has been understating what the floor costs by 59%. Measured on the live ledger on 2026-08-21: it reports $998.98 against a true $2,422.08, so $1,423.10 of real spend is invisible. The cause is that AgentUsageSample.usd counts from process start, not from the agent's beginning. TelemetryCollector accumulates into in memory maps, so an app restart rebuilds them empty and the counter starts again at zero under the same session_id, because the agent resumes and only our accumulator forgot. Anything reading the last value therefore reads one restart's worth of spend. The ledger keeps every line, so the pre reset peaks are still on disk. Within one agent and session the counter only climbs, so a decrease is the reset signature and nothing else. Lifetime is the sum of each closed segment's peak plus the open segment. Any decrease counts. A dollar threshold was considered and rejected: two real restarts in the live ledger drop from $0.92 and $0.73 straight to $0.00, and a $1 threshold silently misses both. A cumulative counter has no legitimate reason to fall, so the magic number bought nothing and cost coverage. Folding is async and incremental because the ledger is already 32MB and a full pass takes about 800ms, which must not land on the Electron main thread. Each pass reads only the bytes appended since the last one. Steady state is a few hundred bytes. fleet.json now carries lifetime in usd and keeps the old figure as sessionUsd, so the LIVE ROSTER line every agent sees is correct without a second change. Until the first fold completes usd falls back to the session figure rather than publishing a cold zero. Verified against the real ledger by a second implementation written separately. Both agree to the cent, with a worst per agent disagreement of $0.000000. The breaker is NOT touched here. Its floor wide cost cap reads the same resetting counter, so it re arms to zero on every restart and measures one app session rather than lifetime. That is a policy change and belongs to the founder, so it is written up rather than fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Rue1dRSfggYgRh5aHzqZn | 2 个月前 | |
test: kill two suite flakes, a shared temp path and an unconditional proxy assert Both were intermittent failures with no owner, and neither is a product bug. update-applied used one fixed name in the machine temp dir for its unwritable-state-dir fixture. Two suite runs on a machine race: the second deletes the sentinel file in its own cleanup, analytics then recreates the path as a real state directory via mkdirSync(recursive), and the first run's non-recursive rmSync throws EISDIR. The directory outlives the run, so from the first collision onward writeFileSync throws EISDIR on every later run for everyone on that machine until a human deletes it. Flake once, then a trapdoor. Now a per-run mkdtemp name, and cleanup removes the whole directory. The crush tests asserted the proxied branch unconditionally. hive.ts gates installCrushConfig behind `if (port > 0)` and degrades on purpose when the hive-proxy sidecar does not report a bound loopback port, logging "proxy bridge ... did not bind". That degradation is correct product behaviour, so the tests were reading a crush.json production had chosen not to write and failing with ENOENT. They now assert whichever branch was actually taken: the full proxied contract when the bridge bound, and when it did not, that routing was left completely untouched rather than half applied. Coverage goes up and the degraded path gains an assertion it never had. Verified by forcing the no-bind path: identical ENOENT before, green after, and green again unforced. Product code is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tjbh3gAgKrkGetDWSX5SCt | 2 个月前 | |
fix(onboarding): check engine availability before the pick is committed The orchestrator step recorded Michael's engine without checking whether the CLI exists on this machine. The first check ran at spawn time in spawnAgentCore, and for Grok, Antigravity and Qwen (no installCommand, no nativeInstallCommand) the install ladder ends at the manual rung: a hint in a terminal and a Michael that never starts. That is a first run failure for a brand new user. The wizard now probes tools:status on mount, badges every engine row as INSTALLED, INSTALLS ON FIRST RUN or NOT INSTALLED, and when the selected engine is missing with no installer it explains what happened and what to do, offers check again and install instructions, and refuses to advance. finish() guards the same way. An unknown or failed probe never blocks. The copy claiming every option is an engine you have installed is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J6Z6jcJ5hP1NCUo5ZHVNGT | 2 个月前 | |
Merge #179: fix(fs): use expandTilde in statAbs for cross-platform path resolution | 2 个月前 | |
fix(focus mode): actually come back in focus mode after a restart Two bugs, and the second one hid the first. 1. The preference was resolved ONCE, while the store was being constructed. Every agent restored from the roster still carries the PTY id it had in the previous session, so the first reconcileWithLivePtys correctly prunes all of them and refocusAfterRemoval correctly returns null. By the time god respawns with a live terminal the preference has already been read and thrown away, so the app opens in the sidebar with cth.prefersFocusMode still set to 1. Re-check it as the roster fills in instead. 2. FullscreenTerminal's re-home effect called setFullscreen, which is the EXPLICIT toggle and writes the preference. So an agent going away silently recorded "the user does not want focus mode". Split off refocusFullscreen for the paths that follow the user rather than being told by them. Same trap as closing an agent in focus mode: fixing the store is not the same as owning the field. Grep the call sites. Restores only onto an agent that HAS a terminal, otherwise the re-home effect bounces straight back out and it is a loop, not a restore. 436 tests, 6 new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018R7wTroJDW1MbHC8Qi9eaY | 2 个月前 | |
fix: confine the file IPC to the workspace when symlinks are involved A symlink inside the selected workspace let the file IPC read and overwrite files outside it. `<workspace>/notes.txt -> ../elsewhere/target` reads as an ordinary in-workspace relative name, and the read/write/list call then reached the external target. Root cause: the shared containment guard was lexical only. `resolve`, `normalize` and `relative` are string math and know nothing about symlinks, while the `readFile`/`writeFile`/`readdir` that runs afterwards is resolved by the kernel, which does follow them. Every consumer of the guard was affected — the text read, the binary read, the write, the directory listing, and both git path operations — because they all share it. The guard becomes an async `safeResolve` that all of them call, in three layers: 1. canonicalize the workspace root and the target with `realpath` and re-check containment on the canonical paths. The not-yet-existing tail of a path is re-attached to its deepest existing ancestor, so creating a new file in a real workspace directory still works. 2. reject any path component that is a symlink, via an `lstat` walk. Not redundant with (1): `realpath` throws ENOENT on a dangling symlink, so canonicalization alone treats it as a name to be created and the write follows the link and creates the file at its external target. 3. open reads and writes with `O_NOFOLLOW` (plus `O_NONBLOCK`, so a FIFO cannot park the open and with it the IPC call), so containment does not rest on the path still meaning the same thing between the check and the open. Those flags are POSIX-only, which is why (2) is a check in its own right and not merely a pre-filter. Layer 3 is why the git diff's working-side read had to change shape rather than just gain a call to the guard. It was a plain `stat` + `readFile` on the cleared path, which is a second and third resolution of a name the kernel is free to look up differently — anything that swaps that final component for a symlink after the check is what the read actually gets, and the external file then travels to the renderer as diff text. It now opens through the shared `openForRead` and stats through the returned handle, like every other read of a confined path, so the flags are stated in exactly one place. The boundary is the WORKSPACE, not symlinks. A link whose target exists and is itself inside the workspace is followed, and what comes back is the canonical path of that target: it reaches nothing the caller could not already reach by the target's real name, and refusing it would make an ordinary `node_modules` or monorepo checkout unbrowsable for no gain. What is refused is a link that leaves the workspace, and a dangling link, whose target does not exist yet and so cannot be shown to land inside it. Both directions of that are pinned by tests, so the policy cannot be flipped by accident later. The lexical form is no longer exported — a caller reaching for it would reintroduce exactly this bug. | 1 个月前 | |
fix: getStatus consumes the second path of porcelain -z rename records Porcelain -z emits rename/copy records with two null-terminated paths ("R <new>\0<old>\0"), but getStatus read every token as a single "XY <path>" record, so the old path parsed as a phantom status entry whose letters came from the filename ("alpha.txt" -> path "ha.txt", index 'a', worktree 'l') and appeared in both the staged and unstaged lists while the real old path was dropped; a 2-char old name was silently skipped instead. Parse it the way parseNameStatusZ already does: consume the source token and keep it on the entry as oldPath (new optional field), so the IDE panel's staged/unstaged lists no longer show files that do not exist. | 1 个月前 | |
fix: god's renamed identity no longer reverts to "Michael" on respawn renameAgent() (store.ts) -> hive.ts's renameAgent() persists a rename straight into registry.json. But useHive.ts's god-spawn effect rebuilt god's agent object from scratch on every respawn with the literal 'Michael' hardcoded in three places (the `hive:` spawn payload, the Agent object added to the store, and the /remote-control session name) instead of reading the persisted name back — so a rename reverted on every app restart even though the registry still had it right. Extracts the name-resolution into a small pure helper, src/shared/godIdentity.ts::resolveGodName(), mirroring the pattern already used elsewhere in this repo for reading god's persisted name (realtime/tools.ts's fleet-status tool: `reg.agents[godId]?.name`). The god-spawn effect now calls window.cth.hiveRegistry() (already used elsewhere in this same file, effect 0) before building the spawn payload, and passes the resolved name through all three sites instead of the hardcoded literal. Closes #279. Tested on macOS (Apple Silicon), Node 26.7. `npm run typecheck` and `npm run build` both clean. `npm run test:focused`: 551 tests, 550 pass; the 1 failure (test/update-download-asset.test.cjs) is a pre-existing artifact of running `npm install --ignore-scripts` in this sandbox (better-sqlite3's native rebuild fails against this machine's V8/Node headers, unrelated to this change) and reproduces identically on a clean checkout before this diff. Added 2 new tests for resolveGodName() via the existing load-ts.cjs harness, both green. | 2 个月前 | |
Cost Grok agents from the usage file their CLI already writes (#536) A Grok agent ran fine and cost $0.00 forever. The telemetry env that makes an agent push OTel is injected for Claude Code alone (hive.ts `ensureAgent`), and a Grok agent writes no Claude transcript, so both of the collector's sources came back empty — no fleet numbers, and no row in cost-ledger.jsonl at all. On this machine that is 21,542 ledger rows, every one of them a Claude model, and none for the Grok agent that had been working alongside them. Its real numbers were on disk the whole time. The Grok CLI keeps a cumulative per-session snapshot at ~/.grok/sessions/<encodeURIComponent(cwd)>/<id>/ usage.json, and both halves of that path are already resolved: the Grok hook bridge normalizes its camelCase payload to `session_id` (GROK_HOOK_SHIM), which `recordSession` stores, so resolveCwd/resolveSessionId answer for a Grok agent exactly as they do for a Claude one. So this adds a reader, not any plumbing. Cost comes from the file too — `costUsdTicks`, 10^10 ticks to the dollar per Grok's own docs — so there are no Grok prices to maintain here. The dedup needs care. `appendCostLedger` is fed a running total, so re-appending an unchanged sample writes the same row forever; that is #56, which left 2,417 identical rows. The transcript fallback dodges it by reporting an empty session id to disqualify itself from the ledger, but this provider cannot — it needs a real session id to be accounted at all. Hence CumulativeSampleGate: admit a row only when the numbers move. An idle Grok agent re-reads the same file every beat and is correctly silent. `ts` is not in the signature, because when only the write time changed there is no new cost to record. The Claude path is untouched. `grokFallback` returns null for a Claude agent — its session id is not a directory under ~/.grok/sessions — and the ledger gate short-circuits on provider before the gate is ever consulted. Closes #535 Claude-Session: https://claude.ai/code/session_01YSioheUKVkWkZ3aRxjYnaJ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 1 个月前 | |
fix(config): expand ~ at the two seams a pre-fix config can still reach The write-side of #140 is fixed on main (normalizeHiveHome), but a config.json persisted by a pre-fix build still holds literal ~/… strings that nothing rewrites: the hive picker renders them and feeds them straight back into config:changeHome, whose resolve() anchors ~ at cwd — the app relaunches against a real directory named "~". Reduced per review to what main still lacks: - config:changeHome expands before resolve (the live seam). - readConfig normalizes harnessHome/recentHives on the way OUT (the reviewer's better-shape suggestion): fixes the picker's display AND the click in one place, and collapses a stale ~/X next to its absolute twin. - test/harness-home-tilde.test.cjs seeds require.cache with an electron stub (userData in a temp dir) so it is independent of the electron binary, covers the upgrade path end-to-end plus the ensureHarnessHome regression and the ok:false path, and is wired into main's current test:focused list. (cherry picked from commit 3a879c52e690ba3432ea3dfe431eaef2e37d811b) | 2 个月前 | |
test(#140): cover the real ensureHarnessHome, Windows-safe path assertions Rebased onto main after the config-side fix landed there; what remains novel from the original PR: - test/harness-home.test.cjs exercises the REAL ensureHarnessHome / writeConfig / readConfig (electron app.getPath stubbed to a temp dir, same require.cache pattern as hive-roster-injection.test.cjs). It replaces the stand-in test in expand-tilde.test.cjs, which only re-ran expandTilde and would still pass with the expandTilde call deleted from the real ensureHarnessHome. - expand-tilde.test.cjs asserted POSIX literals like /a/b/c; resolve() anchors those to the current drive on Windows, so the suite was red there. Compare against path.resolve output instead (six spots, including the #140 recentHives tests that landed on main). - OnboardingWizard error box gets overflowWrap: anywhere so a long ENOENT path cannot blow out the panel width. - Wire harness-home.test.cjs into test:focused. Refs #140 (cherry picked from commit f8df0395f4a314ac35098074fe55fd3c5440da5e) | 2 个月前 | |
feat: start the hive on first run, and add Skills, Prerequisites and release drops THE BOOTSTRAP BUG. `bootstrapHiveServices()` runs once at app-ready and opens with `if (!hive.enabled()) return` — and a fresh install has `harnessHome: null` at that moment. Onboarding then sets it through `config:update`, which did not re-bootstrap. So on every first run the message router (the poll loop draining outbox/ into inbox/), the hook server, the telemetry collector and the mission scheduler stayed dead for the entire session: mail never moved, cards never reported, "Restart & Continue" had no session to resume, and nothing said so. It healed on the next launch, which is exactly why it survived. changeHome had always handled this by relaunching; onboarding does not. Now bootstrapped on the null -> set transition, with the live session id also recorded from the usage tick so continuing works even when a hook never lands. SKILLS. What every agent can already do across Claude Code, OpenCode and Codex, deduped project > user > bundled (the precedence the CLIs themselves apply), plus a browsable catalog parsed from a README with no machine index. Install treats the source as hostile because it is arbitrary content from a public list: sanitised destination names, containment re-checked AFTER path resolution, 60-file/2MB/depth-5 caps, symlink entries skipped, and nothing written until the whole tree resolves. Uninstall refuses any path it cannot prove is a SKILL.md folder inside a managed root, and never touches bundled skills. PREREQUISITES. One Settings page for the tools the app leans on — uv, git, Node, MemPalace, every engine — with live status, the real path when present, and platform-correct commands when not. The button delegates to Michael rather than shelling out, so a real confirmation step sits in front of anything that writes outside the app. RELEASE DROPS. A release body can carry an authored HTML page between <!-- drop --> markers, shown as a centered modal. It renders in an iframe with sandbox="" AND its own default-src 'none' CSP — two independent controls, either sufficient — because the renderer it would otherwise reach has window.cth bridged onto it, where a script is arbitrary code execution with the app's authority. SETTINGS HERO CARD. Version, plan, sponsor and a way to reopen the notes, with contents fetched from docs/hero.json so they change without a build. Remote DATA, never markup: every field is a React text node, URLs must be https, strings are length-capped, and a malformed payload degrades to the compiled-in defaults. Also: OpenCode no longer preselects a BYOK model the user may not own and then silently fall back, and the voice button explains how to enable dictation (including that Groq is free) instead of sitting dead. 31 new tests, 239 total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
feat(i18n): localize long-tail components (T1b) Extend react-i18next coverage to the remaining UI surfaces: SetupPanel, AiEnginesSettings, McpDefaultsSettings, RealtimeDevicePicker, RealtimeMichaelToggle, triggers/* (all tabs + shared ui), MessageQueueComposer, TasksKanban, FileTree, GitTab, FullscreenFileEditor, FullscreenTerminal, MemoryPanel, MemoryGraphPanel, ThreadsPanel, SkillsTab, WorkersTab, IntegrationsRegistry, UpdatesSection, IdePanel, GitPanes, ImagePreview. Locale files grow to 1122 keys each, still perfectly aligned. Adds a scripts/merge-i18n-fragments.cjs helper (batched fragment merge with collision + parity checks). Kept English by design: Slack/Webhook long API docs, HIRE_PROMPT and agent-prompt templates, CLI endpoint hints, and model/engine proper nouns. Co-Authored-By: Claude <noreply@anthropic.com> | 2 个月前 | |
fix(hires): clear cancelled batches and update caps atomically Why: - Cancelling a batch review left unseen manifests queued, so stale hires and progress resurfaced when the modal reopened. - Command Center replaced the whole token-cap map from a renderer snapshot and could erase caps written by the hire flow. What: - Clear the review queue on modal close and reset later imports to a fresh run. - Extend the main-process per-agent cap IPC to atomically set or clear one entry. - Route Command Center cap edits through the atomic IPC and reconcile optimistic state. - Share the cap ceiling constant and remove duplicate manifest application. - Add RED-to-GREEN regressions for cancel/reset, atomic set/clear, and renderer wiring. Risk: - Cap persistence remains best-effort in the renderer; failed optimistic edits are reconciled from persisted config. - No IPC channel name or existing numeric set call changes. Tests: - node --test test/hire-queue.test.cjs test/agent-token-cap.test.cjs test/hire-import.test.cjs (17 passed) - npm run test:focused (256 passed) - node --test test/*.test.cjs (275 passed) - npm run typecheck - npm run build - git diff --check Live Docs: - Not applicable; this completes review feedback on the pending batch-hire PR. (cherry picked from commit 80b9867ba6ec3624516baec27c501699ef0f719d) | 2 个月前 | |
fix(hires): enforce runtime allowlists in gallery validator Why: - The Gallery and hiring desk accepted untrusted flags, skill ids, and MCP ids that the canonical app validator rejects. - Static spec drift could mislabel unsafe manifests as valid. What: - Mirror default-deny command flag sequencing and bundled skill/MCP allowlists in the browser validator. - Align the schema and security spec with current runtime behavior, including Cursor and percent-expansion rejection. - Add differential contract tests across unsafe inputs, field boundaries, allowlists, and every published manifest. Risk: - Browser allowlists remain duplicated static data; parity tests fail if runtime catalogs drift. - JSON Schema draft-07 cannot express flag/value adjacency, so the JavaScript validators remain authoritative. Tests: - node --test test/hire-validator-parity.test.cjs - npm run typecheck - npm run build - Ajv smoke: 80 published manifests accepted and 3 unsafe cases rejected - npm run test:focused (750 passed; 8 unrelated environment failures; 1 cancelled) Live Docs: - Hire spec: docs/hires/spec/HIRE_SPEC.md - JSON schema: docs/hires/spec/hire.schema.json | 1 个月前 | |
review: archived check ahead of the provider branches; the notice is routed, not dropped into an inbox Self-review (CONTRIBUTING: run your agent over your PR). An archived agent on a hookless or proxy-tier engine hit the terminal-handoff branch first: nothing filed, no log, no notice. The archived branch now runs right after the assistant bounce, so with no terminal the inbox is where the mail goes whatever the engine. The notice goes through routeMessage, so a sender on such an engine receives it the way it receives any mail (terminal handoff, or the god bounce when no renderer is there); requires_reply false and the live-sender guard keep it from re-entering. Dropped the redundant sender-is-target guard. Test: an archived custom-engine agent is filed and its sender told; a custom-engine sender is told through routing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> | 1 个月前 | |
test(hive): lock in the cwd/tilde, bundled-node hook and roster-injection fixes Promotes the three throwaway verification scripts from the 913b8ad / 7384906 work into permanent regression tests, rewritten in the repo's own idiom: node --test + test/load-ts.cjs, which transpiles the real sources. No esbuild bundling and no electron stub directory — hooks.ts only reaches for Notification, so the test seeds that one export into the require cache. - expand-tilde the pure expander, incl. the deliberate "a relative path is returned untouched, not resolved" contract. - hive-cwd a "~/…" cwd lands in the registry ABSOLUTE with cwdValid:true, and cwdValidity repairs older entries. - hive-hook-node the launcher is written and executable, all four installers (claude/agy/codex/grok) route through it, and a hook actually fires under PATH=/usr/bin:/bin with its payload arriving at HIVE_SOCK. - hive-roster-injection god-only injection on SessionStart + every prompt, and the merge that stops the roster and a queued operator steer from displacing each other. Each was mutation-checked: reverting expandTilde to identity, the hook command to bare `node`, or the roster to any of its three pre-fix shapes (steer-only, not god-gated, SessionStart-only) fails the corresponding tests. Two notes for whoever touches these next. The stripped-PATH test spawns async, not spawnSync: the shim connects back to a socket the test process is serving, so a sync call deadlocks its own event loop; and the shim never half-closes, so its payload is only complete on 'close'. The installer test redirects HOME and asserts the redirect took effect before running, because agy and grok write into the user's real ~/.gemini and ~/.grok. Added to test:focused — 58 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(hive): avoid rewriting generated docs during runtime operations - make ensureHive() create PROTOCOL.md and COMMANDS.md only when missing - add an explicit refreshGeneratedDocs() path for generated hive docs - refresh generated docs only during bootstrap - add generated notices to PROTOCOL.md and COMMANDS.md - add regressions for writeTasks(), ensureAgent(), explicit refresh, and missing-doc recovery | 1 个月前 | |
Merge pull request #369 from drona23/fix/provider-hook-spaced-paths-364 fix(hive): quote provider hooks for spaced POSIX paths Conflicted with #356 (merged just before it) in two places, both resolved by keeping BOTH sides rather than taking one: test/hive-hook-node.test.cjs — the conflict cut through two different test bodies that begin at the same anchor. #356 adds 'Codex hook commands survive a hive path containing spaces', #369 adds 'POSIX Gemini and Antigravity hooks survive a hive path containing spaces'. Taking either whole side would have deleted the other test. Both are kept whole; the file now has 12 tests and all 12 pass, including both space tests. src/main/hive.ts — a doc comment on nodeRunUnquoted() only, no code. Took #369's wording because after both PRs the helper is reached only from the deliberate win32 branches, which is what that wording describes. Every code change in #369 merged clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EmZGHZkLeKXWxrFVPmUPxW | 1 个月前 | |
fix(hive): make the HOP_CAP loop guard real - harness owns hops and increments per bounce The routeMessage() hop-cap check compared msg.hops against HOP_CAP with a "loop guard - drop a runaway message" comment, but nothing ever incremented hops: normalize copied the agent-supplied value verbatim and all four harness bounce paths spread ...msg unchanged, so a god<->hookless-worker relay loop delivered a duplicate bounce (plus a log line and a git commit) every 1.5s router tick forever, while an outbox JSON echoing hops: 13 - a field PROTOCOL.md says the HARNESS fills in - had its first delivery silently dropped. normalize() now clamps the agent-carried hops into [0, HOP_CAP] (a carried relay count keeps climbing toward the fuse; a poison or negative value can neither fake nor evade it), and the four bounce paths go through a new bounceToGod() helper - the only place the counter moves, +1 per harness bounce - where the cap fuse actually fires and drops with reason 'hop-cap'. The dead check at the top of routeMessage() is removed: with clamped input it could never fire, and that inoperative "loop guard" was the bug. | 1 个月前 | |
fix(hive): recover malformed outbox JSON line breaks - repair literal LF and CR characters inside JSON string values with a bounded scanner - preserve the valid JSON fast path and existing routing behavior - log successful repairs as outbox-repair events - log unrecoverable parse failures as drop/malformed-json before quarantine - add regressions for valid JSON, LF, CRLF, structural corruption, escape handling, and poison-message isolation | 1 个月前 | |
fix(hive): stop stale inbox nudges waking agents against an empty inbox The inbox-wake nudge is QUEUED the moment fresh mail is seen but TYPED only once the agent is idle and off cooldown, and it persists across a renderer reload. Mail that arrives while an agent is mid-turn therefore queues one nudge per 4s poll; the agent drains its whole inbox on the first one and files it to inbox/.done/, and every nudge behind it lands on a directory that agent has already emptied — a delivery slot and a model round-trip each, to be told there is nothing new. With no id in the payload the agent also cannot tell "I handled this last turn" from "I was woken for nothing", so it re-checks and guesses. enqueueMessage already holds exactly this invariant for /compact, for exactly this reason — "idempotent in the worst way: the first does the work and every one behind it answers nothing to compact". The nudge is the second command with that property, so it gets the same rule in the same place. - `@shared/hiveNudge` builds the nudge and recognises it, mirroring `isCompactionCommand`. The text now names the message ids that prompted it. - `enqueueMessage` keeps one nudge pending per agent. Suppressing a copy loses nothing: the surviving nudge sends the agent to the same directory, where the newer mail is waiting too. - the ids are diagnostic, not a work list — a suppressed copy leaves its ids unnamed, so the text keeps the pending inbox authoritative and tells the agent to work everything in it. Delivery itself was already atomic-before-notify (deliver() writes tmp+rename before emitMessage, and the poll reads the real inbox), so this is staleness after the notification, not a write/notify race. | 2 个月前 | |
fix(hive): seed Pi with positional bootstrap prompt - Declare Pi's positional initial-prompt delivery so fresh agents receive the full hive bootstrap instead of spawning bare. - Also tighten the generic provider bootstrap contract, add Pi regressions for single-argv delivery and bridge-install failure, and document intentional bare-spawn behavior for providers without a seed strategy. | 1 个月前 | |
adapt tests, only copy files if they exist | 1 个月前 | |
feat(hive): a 1:1 hold toggle that tells Michael to leave an agent alone The human takes an agent aside; Michael keeps dispatching to it. Both then drive the same terminal, which is how a 1:1 turns into a fight over one session. Title-bar toggle, next to focus mode. It acts on whoever is on screen — the focused agent, else the selection — and the tooltip names them, because a title-bar control that silently picks its own target is how the wrong agent ends up on hold. Michael himself is excluded: telling the orchestrator to stop routing to itself is not a state worth having. onHold lives on the registry record, so it survives a restart. A hold that evaporated on relaunch would hand the agent back to Michael with nobody told. fleet.json is patched in the same operation rather than waiting for the 8s snapshot, for the same reason renameAgent does it: one more dispatch fits in 8 seconds. In the roster the agent gets `ON HOLD — 1:1 with the human`, and the legend gains a block saying do not message, do not dispatch, do not count them when picking an owner, and do not archive or replace them either — they are still running. The block appears only while something is held; guidance that is always on reads as boilerplate and gets skipped. The button mirrors state only after main confirms the write. Flipping optimistically would show a hold Michael never heard about. 456 tests, 5 new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018R7wTroJDW1MbHC8Qi9eaY | 2 个月前 | |
fix(runtime): bundled node on the agent PATH; stop offering an installer that cannot run Layer 2 — Gap B. Layer 1 routed the commands WE generate through hive-node. This covers the ones we don't: an MCP server declared as `node ./server.js`, a provider CLI that shells out to node, a helper an agent wrote itself. With no system node those die with 127 — the same bug, one level down. <hive>/bin/runtime/node → the same ELECTRON_RUN_AS_NODE trick, named `node`, and APPENDED to the agent's PATH. Appended, never prepended: a user who has their own node keeps their own version and we are strictly the fallback. Prepending would silently swap the node version under the user's own projects for Electron's. Deliberately no npm/npx shim — Electron bundles the Node runtime, not the npm CLI, and a stub would fail confusingly rather than be honestly absent. Layer 3 — Gap A. Every provider's installCommand is `npm install -g …`, which needs npm, which needs node. On a machine with no node the missing-CLI banner printed that command and RAN it, so a fresh user watched `npm: command not found` scroll past and concluded the app was broken. Now the ladder classifies first: npm present → npm install (unchanged; the common case) npm absent + native installer → the vendor's self-contained one (Claude Code's) npm absent + no native → manual hint naming the ACTUAL missing piece; runs nothing, and the auto-relaunch is not armed. Never auto-installs a system Node, touches nvm, or edits a shell rc. The ladder moves to src/main/cliInstall.ts, which imports nothing from electron, so both the decision and the emitted script are testable without booting an app. 13 tests. Mutation-checked: prepend-instead-of-append, shims-never-written, ladder-ignores-npm and manual-rung-runs-npm-anyway each go red. | 2 个月前 | |
Fix race-safe realtime task writes | 2 个月前 | |
fix(hive): bounce and log mail to an id with no inbox instead of dropping it `deliver()` returned early when `agents/<to>/inbox` did not exist. Its own comment said the drop was "logged by caller" — the caller never logged it, so the message was destroyed while `routeOnce` archived the outbox file to `.sent/` and `log.jsonl` recorded `kind:"message"`. Every observability surface reported a delivery that never happened. Unknown-recipient was the only delivery failure with neither a bounce nor a drop entry: the prep-assistant and hookless-provider cases already bounce to god, and the hop-cap case already logs `kind:"drop"`. It reaches `deliver()` because `reg.agents[t]` is `undefined` for an unknown id, so `canReceiveInbox(undefined)` falls back to the claude preset and returns true. - `deliver()` returns whether it wrote, and the no-inbox case now logs `{ kind: 'drop', reason: 'no-inbox' }` and bounces to god, mirroring the existing undeliverable bounces. - the `kind:"message"` entry carries `delivered` — the targets that actually took delivery — so the log distinguishes "agent ignored it" from "it never arrived". `terminal-handoff` already logs the same field. Closes #183 | 2 个月前 | |
fix(windows): stop cmd.exe from eating the hive protocol prompt On Windows, agents could not message each other. Not the socket (the hive already uses a named pipe there, and routing is filesystem polling anyway) — argv destruction by cmd.exe. A `.cmd` cannot be handed to CreateProcess, so any resolved target that is not `.exe`/`.com` was spawned as `cmd.exe /d /s /c "<one pre-escaped string>"`. The hive protocol rides in on argv (`--append-system-prompt <prompt>` for Claude, `--prompt <prompt>` for OpenCode) and that prompt is ~6.1k chars, 11 lines and 62 parentheses. cmd.exe treats CR/LF as a statement separator before quoting is even considered, so the argument was truncated at its first newline — the agent booted, looked completely healthy, and never received the HIVE PROTOCOL block that tells it `inbox/` and `outbox/` exist. It therefore never wrote mail, and nobody ever heard from anybody. Claude alone appeared to work because it has a native `claude.exe` and skipped cmd.exe entirely; OpenCode is npm-only (`%APPDATA%\npm\opencode.cmd`), as is `claude.cmd` on a machine without the native installer. Escaping could not be repaired into a fix. cmd.exe has no backslash escape at all — `\` is literal and every `"` toggles quote state, so the `\"` this code emitted (and a comment described as "cmd's quote-escape") actually left the following text OUTSIDE quotes — and no escape exists for a newline in any case. The command line was also near cmd's 8191-char ceiling. So stop using cmd.exe for these spawns. `parseNpmCmdShim` decodes an npm shim (all three historical cmd-shim shapes, plus the pnpm/yarn `..` variant) into the interpreter and script it wraps, and the spawn path launches that interpreter with an argv ARRAY. node-pty then applies its own MSDN/CRT escaping and hands the result straight to CreateProcess: newlines, parens and quotes are ordinary characters inside one quoted argument, and the ceiling becomes 32767. Conservative by construction. The resolver is pure (path + content in, no fs, no platform check) so it is unit-testable on macOS where this was written, and it returns null — falling back to today's exact cmd.exe behaviour — for anything it does not fully understand: unknown interpreter, unexpanded `%VAR%`, relative target, non-JS target, extra interpreter flags it would otherwise silently drop, oversized file, garbage. The spawn-side wrapper adds the impure checks (script present on disk, interpreter resolvable to a real `.exe`) and is gated on `process.platform === 'win32'`, so macOS/Linux are provably untouched. Also fixed, same Windows-only class: - Agent-facing text no longer emits POSIX shell syntax. `"$HIVE_NODE" "$KG_CLI" search …` and the Slack reply command expand to NOTHING under cmd.exe/PowerShell, so those instructions were dead on every Windows floor. Both now bake the absolute resolved path, which is correct on every platform and needs no expansion (and stays prompt-cache-stable). - Protocol paths are built with join() instead of concatenating `/`, so a Windows agent is told `C:\…\agents\god-1\inbox`, not `C:\…\agents\god-1/inbox/`. - The OpenCode plugin is written to BOTH `.opencode/plugin/` and `.opencode/plugins/`. The bridge is LIVE-UNVERIFIED and current docs say plural while the code assumed singular; writing both costs ~2KB and removes a coin flip whose losing side is silent. - OpenCode gains a `nativeInstallCommand` for the node-free install rung (opencode.ai ships no `install.ps1`, so Windows uses the documented Chocolatey route). Tests: `test/win-cmd-shim.test.cjs` pins the resolver against real shim shapes and every refusal case, and proves — using node-pty's own escaper and a CommandLineToArgvW parser — that a hostile prompt round-trips exactly through the array path while cmd.exe truncates it. `test/hive-windows-prompt.test.cjs` pins the prompt/path/plugin changes against the real injected prompt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
test(hooks): add hook event contract regression coverage Add regression coverage for the shared hook event contract. Tests cover: - known hook event payloads - forward-compatible provider event names - optional agent ids - malformed optional fields - main and preload usage of the shared contract This protects the IPC payload shape without constraining the existing multi-provider hook event flow. | 2 个月前 | |
fix(hooks): make an unbound hook socket loud, self-healing and visible For a whole session hooks.sock did not exist while the app was up: every hook's connect() failed and exited 0 (which the CLI reads as allow), the breaker was inert, fleet.json never appeared, no cost was recorded, and nothing said so. HookServer.start() returned silently when the hive had no root yet, left the outcome of listen() to a console.error nobody reads in a packaged app, never retried and never re-checked. Now every bind outcome is logged to the console and to the hive's own log.jsonl, a failed bind retries with back-off and toasts once per outage, fleet.json carries a `hooks` health field, and the beat verifies every 15s that the path still reaches our listener — a stat of the file we bound, then a ping the server answers with its own instance id — and re-binds when it does not. One way a live socket vanishes under a running app is closed for good: libuv unlink(2)s a Unix socket PATH by name when the server closes, so an old instance that hangs on quit and closes its server after a relaunched instance has bound the same path deletes the new socket. stop() and the beat now only close a listener whose path still leads to the file it bound; otherwise the handle is abandoned unref()ed and the path is never unlinked. The connection handler moves into serve(), unchanged apart from answering the ownership ping before handle(); the bounded frame decoding from #400 is kept as is. start() still publishes the net.Server synchronously — listen() binds and creates the socket file before returning — so callers that look right after it see the handle. The two oversized-frame tests in hooks-framing.test.cjs now read only the rejection entries from the log, since the bind is recorded there too. Six tests on real sockets in test/hooks-socket.test.cjs; two of them fail on main for behavioural reasons (no record of the bind, and one instance's stop() deleting another's live socket). Closes #277 | 1 个月前 | |
test(hooks): cover Notifications/Stop idle-detection branches HookServer's idle-notification logic (notification_type === 'idle', falling back to string-matching 'waiting for your input', plus Stop/SubagentStop) had no test coverage. Adds 8 cases covering both branches the case-insensitive string fallback, permission-request exclusion, stop_hook_active dedup, and the notifications:false toogle. | 2 个月前 | |
fix(hooks): make an unbound hook socket loud, self-healing and visible For a whole session hooks.sock did not exist while the app was up: every hook's connect() failed and exited 0 (which the CLI reads as allow), the breaker was inert, fleet.json never appeared, no cost was recorded, and nothing said so. HookServer.start() returned silently when the hive had no root yet, left the outcome of listen() to a console.error nobody reads in a packaged app, never retried and never re-checked. Now every bind outcome is logged to the console and to the hive's own log.jsonl, a failed bind retries with back-off and toasts once per outage, fleet.json carries a `hooks` health field, and the beat verifies every 15s that the path still reaches our listener — a stat of the file we bound, then a ping the server answers with its own instance id — and re-binds when it does not. One way a live socket vanishes under a running app is closed for good: libuv unlink(2)s a Unix socket PATH by name when the server closes, so an old instance that hangs on quit and closes its server after a relaunched instance has bound the same path deletes the new socket. stop() and the beat now only close a listener whose path still leads to the file it bound; otherwise the handle is abandoned unref()ed and the path is never unlinked. The connection handler moves into serve(), unchanged apart from answering the ownership ping before handle(); the bounded frame decoding from #400 is kept as is. start() still publishes the net.Server synchronously — listen() binds and creates the socket file before returning — so callers that look right after it see the handle. The two oversized-frame tests in hooks-framing.test.cjs now read only the rejection entries from the log, since the bind is recorded there too. Six tests on real sockets in test/hooks-socket.test.cjs; two of them fail on main for behavioural reasons (no record of the bind, and one instance's stop() deleting another's live socket). Closes #277 | 1 个月前 | |
Merge PR #205 (i18n: en + zh-CN), with the orchestrator's name restored The founder overruled our defer: there are real users in China and the Gulf, and he wants accessibility, explicitly accepting partial translation with an English fallback. So this lands. Two mandatory changes came with it, and the first turned out to be bigger than the brief. 1. THE NAME PASS The orchestrator is renameable, and 43 locale strings called it "Michael" in both en and zh-CN. Merging as-is would have silently reverted a rename everywhere at once, which is a bug this codebase has already fixed three times in the spawn path (c461fe1a, 08489c1c, 46737e52). Worse, and not in the brief: PR #205 also replaced PER-AGENT runtime names with the literal "Michael", in the locale files AND in code. SchedulesSection.tsx had `agents.find((a) => a.isGod)?.name ?? 'the orchestrator'` rewritten to the string 'Michael' in three places, and WorkersTab/TriggerHistoryTab lost their godName lookup entirely. The user-visible result: * "Michael has no live terminal." on any agent you clicked * "Michael runs the floor" on every agent's own card * "This restarts Michael; a conversation on a different engine can't be resumed." on the confirmation for restarting a DIFFERENT agent That last one is a destructive-action dialog naming the wrong target, so it is a correctness bug, not a translation nit. Fixed by splitting the strings in two. Strings about the orchestrator now say {{godName}}, supplied through i18next's `defaultVariables` so no call site has to know the name and a rename reaches every string in both locales at once (useGodNameSync pushes the live name in; the languageChanged emit is what makes react-i18next re-render). Strings about one agent say {{name}} and their call sites pass it. 2. ENGLISH DEFAULT detectLanguage() returned zh-CN whenever navigator.language started with zh. An existing user on a Chinese-locale machine would have had the UI switch out from under them on upgrade, having never asked for it. It now returns the saved choice or English, and never reads the OS locale. It also validates the saved value against the supported list. CONFLICTS: 43 hunks across 12 files, resolved toward HEAD's semantics and #205's t() calls. Two worth noting. OnboardingWizard's autonomy comment came back with stray quotes injected before every em-dash and arrow, and reverted the codex flags from `-a never -s workspace-write (sandbox kept)` to `--dangerously-bypass-approvals-and-sandbox`; HEAD kept. OfficeFloor's GPU notice kept HEAD's floorNote() helper with #205's key. Verified by mutation: putting "Michael" back in one string fails 3 of 9, pointing a per-agent string at godName fails 2, restoring OS auto-detect fails 1, dropping defaultVariables fails 1. A placeholder-drift test caught a real bug while writing this: upper-casing produced {{GODNAME}}, a different variable that would have rendered literally on screen. STILL OPEN, deliberately: about a dozen strings elsewhere lost a non-name runtime value (memoryPanel.pill dropped the model, skillsTab.installed dropped its count, queueComposer.voice dropped the error). Cosmetic, listed in the report, not fixed here. Suite 643/643 outside the sandbox. Both typechecks clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 1 个月前 | |
fix: confine the file IPC to the workspace when symlinks are involved A symlink inside the selected workspace let the file IPC read and overwrite files outside it. `<workspace>/notes.txt -> ../elsewhere/target` reads as an ordinary in-workspace relative name, and the read/write/list call then reached the external target. Root cause: the shared containment guard was lexical only. `resolve`, `normalize` and `relative` are string math and know nothing about symlinks, while the `readFile`/`writeFile`/`readdir` that runs afterwards is resolved by the kernel, which does follow them. Every consumer of the guard was affected — the text read, the binary read, the write, the directory listing, and both git path operations — because they all share it. The guard becomes an async `safeResolve` that all of them call, in three layers: 1. canonicalize the workspace root and the target with `realpath` and re-check containment on the canonical paths. The not-yet-existing tail of a path is re-attached to its deepest existing ancestor, so creating a new file in a real workspace directory still works. 2. reject any path component that is a symlink, via an `lstat` walk. Not redundant with (1): `realpath` throws ENOENT on a dangling symlink, so canonicalization alone treats it as a name to be created and the write follows the link and creates the file at its external target. 3. open reads and writes with `O_NOFOLLOW` (plus `O_NONBLOCK`, so a FIFO cannot park the open and with it the IPC call), so containment does not rest on the path still meaning the same thing between the check and the open. Those flags are POSIX-only, which is why (2) is a check in its own right and not merely a pre-filter. Layer 3 is why the git diff's working-side read had to change shape rather than just gain a call to the guard. It was a plain `stat` + `readFile` on the cleared path, which is a second and third resolution of a name the kernel is free to look up differently — anything that swaps that final component for a symlink after the check is what the read actually gets, and the external file then travels to the renderer as diff text. It now opens through the shared `openForRead` and stats through the returned handle, like every other read of a confined path, so the flags are stated in exactly one place. The boundary is the WORKSPACE, not symlinks. A link whose target exists and is itself inside the workspace is followed, and what comes back is the canonical path of that target: it reaches nothing the caller could not already reach by the target's real name, and refusing it would make an ordinary `node_modules` or monorepo checkout unbrowsable for no gain. What is refused is a link that leaves the workspace, and a dangling link, whose target does not exist yet and so cannot be shown to land inside it. Both directions of that are pinned by tests, so the policy cannot be flipped by accident later. The lexical form is no longer exported — a caller reaching for it would reintroduce exactly this bug. | 1 个月前 | |
fix(i18n): an IME candidate Enter no longer sends, searches or commits A CJK, Japanese or Korean user types into an input method editor, gets a candidate list, and presses Enter to CHOOSE a candidate. Every `e.key === 'Enter'` handler in the renderer treated that as "go", so picking a candidate sent the message, ran the search, or committed the rename with half-composed text still in the box. The user then retyped work the app had already sent badly. This is live in the shipping build and has nothing to do with the i18n PR: it hits anyone on an IME regardless of UI language. Add `isComposingKey` in src/shared/imeGuard.ts and apply it to the 11 text-entry Enter handlers. Two signals, because one is not enough: 1. `isComposing`, read through `nativeEvent` because React's synthetic keyboard event does not re-expose it. 2. `keyCode === 229`, Chromium's "IME is processing this" sentinel. It covers the race at the end of a composition where compositionend and the confirming keydown arrive in an order that leaves `isComposing` already false for the very Enter we have to swallow. A genuine Enter is always keyCode 13, so 229 cannot swallow a real keypress. The three Enter handlers that pair Enter with Space (AgentCard 136 and 279, FullscreenTerminal 772) are button activation, not text entry, and are left alone deliberately. Verified by mutation: disabling the guard fails 4 of 7, checking only `isComposing` fails 1 of 7 (the race case), and reading the synthetic wrapper instead of nativeEvent fails 3 of 7. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 1 个月前 | |
feat(knowledge-graph): v1 vertical slice — ingest, store, agent CLI (flag OFF) Phase 2 of the enterprise Knowledge Graph. Behind knowledgeGraph.enabled (default OFF → zero behaviour change). Ships ingest → file-backed store → agent retrieval end-to-end for two modalities (text/markdown/code + images). - src/main/kg-core.cjs pure-JS core (parse/chunk/keyword-search), no native deps so it loads under both Electron main AND a plain node CLI (dodges the better-sqlite3 electron-ABI trap) - resources/kg.cjs agent-facing CLI: `kg search|list|get`, invoked via Bash like MemPalace; resolved + env-injected like md-slack-reply.cjs - src/main/knowledge.ts KnowledgeManager: active()/env()/status()/ingest/search, mirrors MemoryManager and slots into the spawn flow - config + spawn + prompt: knowledgeGraph flag; KG_ROOT/KG_CLI/KG_CORE injected when on; one flag-gated, cache-safe line in injectedPrompt - IPC + preload + Settings toggle (+ add-files picker); HarnessConfig mirrored across main/renderer/preload - test/kg-core.test.cjs 12 tests: tokenize/chunk/score + ingest→search round-trip for text & image + the real out-of-process kg CLI typecheck (node+web) clean; node test/kg-core.test.cjs green; build copies the sidecar and the bundled main keeps a runtime require('./kg-core.cjs'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 4 个月前 | |
test: let the TypeScript test loader handle .json imports The node:test TypeScript loader hands every resolved local import to ts.transpileModule, including `.json` ones. TypeScript cannot emit for JSON input and throws "Debug Failure. Output generation failed", so any module that imports a JSON file was untestable, and the failure surfaced as a crashed test file rather than a readable error. The app compiles with resolveJsonModule and the bundler parses JSON imports directly, so do the same here: JSON.parse the file instead of transpiling it. | 1 个月前 | |
fix(memory): arm the mine loop when mempalace is installed after boot Semantic memory could read "On — getting ready…" indefinitely. Only restarting the app cleared it, which made a working setup look broken. start() runs once at boot and returns early when mempalace isn't on PATH. The usual install order defeats that: you open the settings panel, learn you need mempalace, install it — minutes after boot. Nothing re-invoked start(), so the mine loop never ran; the palace is created by the first `mempalace mine`, so it never appeared; and `initialized` is existsSync(palace), so it stayed false. Meanwhile the status poll calls resetBinCache() every time, so `available` correctly flipped true. Available but never initialized is exactly the state the pill renders as "getting ready", and nothing was ever going to move it. The status poll is the one thing that reliably observes the install, so it now re-arms: MemoryManager.refresh() = resetBinCache + an idempotent start(). Boot still calls start() directly. Repeated polls cannot start a second loop — the palace permits a single writer — and `mempalace init` is still deliberately never run, since it ends in an interactive "Mine now? [Y/n]" that --yes doesn't cover and that hangs a spawned child. | 2 个月前 | |
fix(memory): NaN embeddings on Apple Silicon — pin mempalace device to cpu on macOS mempalace's embedding_device=auto picks CoreML on Apple Silicon; CoreML runs the quantized embeddinggemma ONNX graph partially and its fp16 partitions overflow, so every vector (index AND query) comes back NaN and nothing is ever indexed or recalled. Per review: the pin never overrides a user's own MEMPALACE_EMBEDDING_DEVICE (explicit choice wins, and =coreml stays available as the one-command NaN repro); scope stays macOS-wide because it is free for the other model — chromadb's ONNXMiniLM_L6_V2 unconditionally prunes CoreMLExecutionProvider, so minilm never ran on CoreML with or without the pin, and embeddinggemma is the only path that reaches it. Resolution extracted into an exported pure mempalaceDevice(platform, envOverride) pinned by test/mempalace-device.test.cjs. Switching device does not invalidate an existing palace (same model, same 384-dim vectors) — no re-index needed. | 2 个月前 | |
test(hive): pin the two MINE_IGNORE_LINES copies together Integration-branch addition, not part of any PR — drop this commit if you would rather the author add it. #128 duplicates the ignore list across hive.ts and memory.ts with a MUST STAY IN SYNC comment; drift there is silent until a hive repo bloats again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
feat(models): ship a model without shipping a build, and add Fable 5.1 / GPT-6 Astra / Gemini 3.7 Flash Adding a model meant editing renderer source, type-checking it and cutting a release. It is now one line in docs/model-catalog.json on main: the pickers fetch that file at runtime and every installed copy picks it up within the TTL. The mechanism is the one the Settings hero card already uses, not a new one. src/main/fetchText.ts does the https hop, a total parser validates the SHAPE separately from the JSON, and the result is cached under userData behind a six hour TTL. Applied here: - src/shared/modelCatalogPayload.ts — the catalog types (lifted out of the renderer so main can validate against the same shape) and parseModelCatalog, which never throws and returns null for "keep what you have". - src/main/modelCatalog.ts — fetch, cache, and RE-VALIDATE the cache on read. The cache is a file a previous build wrote; a schema bump or a hand edit must not reach a picker unchecked just because it once passed. - IPC models:catalog, a prewarm in app.whenReady() so the cache is fresh before any modal opens, and the preload bridge. The catalog compiled into the build stays the floor. It is what renders offline, on first launch, and whenever the remote copy is missing, unreadable, or announces a schema version this build does not know. The merge is per PROVIDER: a provider the remote copy names replaces that list, one it omits keeps the built-in list. A bad edit therefore costs a list, never a picker. The payload is data and never markup. Every field lands in an <option> as an escaped text node, and a model id is length-capped and stripped of control characters before it can reach a --model flag on a spawn command line, which is the one genuinely sharp edge on this path. AGENT_MODELS becomes agentModels(). A const array is captured at module load and would still be showing the baked list after a refresh landed. The three new models, with every id read off the CLI that will be invoked with it rather than inferred from a name: - claude-fable-5-1, present in the installed claude binary. No [1m] variant. - gpt-6-astra, from codex-rs/models-manager/models.json at rust-v0.153.2. Note it landed in codex 0.153.1, so an older codex will reject the slug; the catalog's bounds constrain the APP version, not the CLI version, so that cannot be expressed here and the command field stays editable. - Gemini 3.7 Flash High/Medium/Low for antigravity, using agy's display label as the id per the existing verified convention, plus the two Cursor carries. Tests: model-catalog-remote.test.cjs covers junk payloads, prototype pollution, the command-line splice, the caps, and every fallback. The SHIPPED literals in model-catalog.test.cjs and the codex ids in provider-config.test.cjs are updated on purpose — they pin the exact --model values the CLIs are invoked with, so a typo there is a model that silently fails to spawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKx1g6LQ6Mmzto5BRY8zLT | 1 个月前 | |
catalog: the Antigravity row for Gemini 4 Argon says "(ultra plan)" The label of the row becomes "Gemini 4 Argon · High (ultra plan)" in docs/model-catalog.json, src/shared/modelCatalog.json and the pinned list in test/model-catalog.test.cjs. The id is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> | 13 天前 | |
feat(runtime): install a real system Node+npm when the machine has none Founder decision (2026-08-07) REVERSES the earlier "never auto-install a system Node" constraint. A user whose machine has no Node could previously only ever get Claude Code's node-free native installer — leaving them with no runtime for MCP servers, hooks, or any other provider. Now the default is to fix the machine. New rung `node-then-npm`, ranked ABOVE `native`: npm usable -> npm install -g <cli> (unchanged) npm absent/too old + installer -> install Node LTS, THEN npm install -g npm absent + native installer -> vendor's self-contained installer neither -> manual hint, runs nothing "Usable" means npm is present AND its Node is >= NODE_FLOOR_MAJOR (20, Electron's own bundled line). Their Node newer than ours -> untouched. src/main/nodeInstall.ts resolves the latest LTS off nodejs.org/dist — newest-first index.json, first entry with a truthy `lts` (NOT index[0], which is the current/odd release) — and pins the artifact's digest from SHASUMS256.txt, which is authoritative: index.json's `files` array omits win-arm64-msi even though node-<v>-arm64.msi is published, and its osx-x64-pkg entry actually denotes the single UNIVERSAL node-<v>.pkg. No digest -> no install, because we run the result as root. The script downloads, VERIFIES, then elevates — visibly, in the same PTY as every other installer, so the user types their own password. Never a silent elevation. Any step failing aborts before the next. Fetches are timeout-bounded (6s): offline just drops to the next rung. Also: pty.commandPath() (probe the binary an agent would really run) and a corrected hive.ts comment — Electron 32.3.3 bundles Node 20.18.1, not the v22 line the old comment claimed. 15 new tests in test/node-install.test.cjs, all offline (injectable fetch, pure script builders). Mutation-verified: reversing the rung order, taking index[0], skipping the digest check, letting a mismatch or a failed download fall through, and quoting the Windows path each go red. | 2 个月前 | |
fix(office): retry a floor rebuild that cannot get a WebGL context When the GPU process dies — a driver reset, a TDR, a Chromium GPU crash — the floor loses its context and glRecovery rebuilds it 1500ms later. If the GPU process has not come back by then, getContext() returns null and Pixi reports it as 'This browser does not support WebGL. Try using the canvas renderer'. OfficeFloor painted that stack onto the floor and stopped, so a condition that clears itself in a second or two cost a full restart, behind a message that blames the browser for something the browser did not do. Classify that rejection and rebuild through the existing generation-bump path, budget held in a ref so it survives the rebuilds it schedules. Out of budget, show a note that names the actual cause and what to do about it. Anything not recognised as a context failure still reports its stack, so a broken theme or tileset is not hidden behind six seconds of retries. Note this is NOT the eviction path: a getContext() that pushes past Chromium's ~16-context cap evicts somebody else and succeeds, so the rebuild always wins its slot back. Verified both ways over CDP against the shipped v0.4.5 build. Policy lives in glRecovery.ts with the loss path, testable with no GPU or Pixi. | 2 个月前 | |
fix(terminal): let Claude Code follow the app theme instead of pinning it at spawn Founder found a black message highlight sitting in a cream terminal. Different mechanism from the OpenCode fix, same shape of bug. Claude Code does not query OSC 11 at all, so answering that did nothing for it. It does support DEC 2031, but we were writing a literal theme: light or dark into its per-session settings, and a pinned theme has nothing to reconsider when a notification arrives. It now gets theme: auto, which is the value that listens. Also closes a gap in our own half of 2031. The protocol expects the terminal to report the CURRENT theme the moment a program opts in, not only on the next change. We set a flag and said nothing, so a CLI set to follow the terminal had nothing to go on at startup and fell back to its own default. That is most of why this looked broken: the flip was never the only missing piece, the opening statement was missing too. An unreadable background reports dark, since a light-styled TUI on a dark background is unreadable where the reverse is only ugly. | 2 个月前 | |
fix(memory): back the mine loop off while the palace is quarantining Follow-up to d360d67, completing item 2 of the BUG-006 fix. Mining into a palace that just quarantined buys nothing: the segment it rebuilds is the same one the next open quarantines again, so the only product of a fast cadence is another copy for the reaper to clean up. Each pass that sees a fresh quarantine now doubles the wait; the first clean pass drops straight back to the base interval. Capped at 30 minutes on purpose. On a palace stuck in the loop the backoff would otherwise climb forever, and waiting has a real cost — a memory is not searchable until it has been mined. The reaper already removes 100% of the disk growth, so this is only trimming CPU and IO. Detection is by newest quarantine STAMP, not by counting directories: the reaper deletes them, so a count is no signal. The boot sweep seeds the stamp before the first mine, so an existing backlog does not read as "just quarantined" and back the loop off before it has mined anything. setInterval becomes a self-scheduling setTimeout so the gap can vary. It is armed synchronously — mineTimer is the "loop is running" signal refresh() reports on, and setting it only after the first mine resolved reported the loop as dead for a whole pass, which the re-arm-after-install test caught. 451 tests, 5 new. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018R7wTroJDW1MbHC8Qi9eaY | 2 个月前 | |
fix(hive): preserve Pi tool identity across hook events - prefer Pi's confirmed toolName and input fields while retaining legacy fallbacks - normalize PreToolUse and PostToolUse through the same bridge mapping - keep malformed and unserializable inputs fail-open without collapsing fingerprints - add Pi bridge regressions for event mapping, socket failures, and breaker behavior | 1 个月前 | |
fix(ci): the evidence gate could never see the section it was judging The section matcher ended `(?=\n#{1,6}\s|$)` under the 'm' flag. With 'm', `$` matches at the end of EVERY line, and the capture group is lazy, so it stopped at the blank line our own PR template puts after each heading. The captured section body was therefore always the empty string, hasEvidence('') was always false, and every PR failed with "Missing evidence: before and after" no matter what was attached. Confirmed red on #333, #329, #327 and #326 before this change. The check only ever passed by accident, when evidence sat on the line immediately after the heading with no blank line, and even then it saw only that one line. Replace the multiline `$` with an absolute end-of-input assertion. The tests read the regex out of the workflow file rather than restating it, so bringing the multiline anchor back fails here instead of on a contributor's PR. Verified by mutation: 4 of the 6 fail against the old regex. This does NOT unblock #333 on its own. Its evidence is a console transcript and EVIDENCE only matches an image or video, which the last test records so that changing that policy has to be deliberate. | 1 个月前 | |
fix(procKill): never group-SIGKILL a recycled pgid — verify membership before the delayed sweep ensureKilled()'s 4s-delayed hardKillTree fired kill(-pid, SIGKILL) with no check that the pid still names the original group. Once the leader and its whole group are gone, the kernel can recycle that pid as the pgid of an unrelated process group; under fork-heavy load (parallel pytest suites) this happened within the grace window and executed innocent bystanders — observed live 2026-08-30: two SIGKILL waves reaped six Claude background tasks (hive T1007). The sweep now snapshots the group's members (pid + ps lstart) at schedule time and re-checks at fire time: reap only when at least one original member is still present (the orphan-reaping case this helper exists for); skip when the group is empty or every member is a stranger; fall back to the old sweep-blind behavior only when ps itself fails, so nothing leaks. win32 path unchanged. Shipped as a hot-patch to the installed app.asar the same night and proven by a 6000-pid fork-storm canary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Poe7Z9nsqt9AAy4EuHhNLb | 1 个月前 | |
feat(provider): add Cursor Agent CLI (`agent`) engine preset Register Cursor as a first-class, god-eligible provider so Michael and workers can run on Cursor credits (default orchestrator model: gpt-5.6-luna-high). Interactive spawn (no -p) keeps the TUI alive for hive mail via type-into-tui seeding; auto-mode uses --force --trust. Co-authored-by: Cursor <cursoragent@cursor.com> | 2 个月前 | |
catalog: bring the built-in model list up to the remote list The list compiled into the build (src/shared/modelCatalog.json) now has the same rows as docs/model-catalog.json: ten rows that were only in the remote list. The pinned lists in test/model-catalog.test.cjs and test/provider-config.test.cjs carry the same rows. Rows only; version stays 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> | 19 天前 | |
hive: retry the proxy-bridge bind and surface a degraded spawn A transient sidecar bind failure used to degrade a proxy-tier agent (Crush) for its entire session: one log line, no retry, nothing the user sees. - startProxyBridgeWithRetry: up to three attempts with a short backoff; each attempt kills the previous sidecar so nothing leaks. - Still failing after that is deliberate degradation and stays so (routing untouched, the CLI runs), but it is no longer silent: log.jsonl gets a proxy-degraded event, the renderer gets hive:degraded, the spawn result carries a one-line reason, and main shows it as a native toast on the same notifications gate the breaker uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5X7vQmQDwrTCn2qBzuBiJ | 2 个月前 | |
fix(pty): agents no longer inherit the parent Claude session's identity The app is often launched from inside a Claude Code session, so the parent's identity markers flowed into every agent PTY. CLAUDE_CODE_CHILD_SESSION makes the agent CLI silently disable transcript saving, breaking --resume for every agent of the run. Per review: strip by prefix (/^CLAUDE(CODE|_)/) instead of a hardcoded list that was already seven markers behind a live session's dump; strip the INHERITED layer only, so deliberate per-agent values survive; keep a small config-not-identity allowlist (CLAUDE_CONFIG_DIR, OAUTH token, Bedrock/Vertex switches); extract the whole layering into an exported pure buildPtyEnv() (src/main/ptyEnv.ts) and pin it with test/pty-env.test.cjs. (cherry picked from commit eca75c457943d1742854f6b626bb6d89d5247ad0) | 2 个月前 | |
test(hive): cover the delivery-time precondition gate Follow-up to e7c7e84. I claimed this repo had no test runner and verified the gate in a throwaway harness instead. That was wrong — `npm run test:focused` runs 30 files through `node --test`, one of which already covers this exact module. The checks that told me otherwise looked for `*.test.ts` (they are .cjs) and a script named `"test"` (it is `test:focused`), so both reported absence because they could not have found it, not because it was not there. So the gate moves out of the useHive closure into queueDelivery.ts as `checkPrecondition`, next to the delivery helper it belongs with, and gets four real cases in the existing test/queue-delivery.test.cjs: mail still pending -> send, inbox drained before delivery -> drop, inbox unreadable -> send (fails open), and no precondition -> send without ever reading the inbox. Behaviour is unchanged from e7c7e84; it is now pinned by the suite instead of by my word. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(windows): finish the will-quit analytics flush with app.exit, not app.quit Manual testing of the packaged build caught a second quit bug: after 'kill all & quit' the agent trees died (the killAll sweep works) but the app's OWN process group — main, GPU, network service, portable stub — survived forever, idle with zero windows. Traced with an instrumented app.asar: teardownAndQuit runs inside the app:confirmClose IPC invoke while the window is still open, app.quit() closes the window, will-quit preventDefaults once to race the analytics flush against its 1.2s cap — and the re-entrant app.quit() finisher is then a silent no-op. No second before-quit, no will-quit, no quit event: Electron's internal is-quitting state survives the preventDefault in this interleaving, so the quit machinery never restarts. The plain window-close path is immune (the window is already gone when quit starts), which is why dev testing never showed it. By flush time teardown has already run, so finish with app.exit(0) — unconditional, and verified against the packaged build: both quit paths now exit cleanly with zero surviving processes. The Electron regression-test fixture now mirrors the app's real quit flow (open window, IPC-triggered teardown, will-quit latch, app.exit finisher) and the test requires a clean exit 0. The wedge itself only reproduces with the full app — the minimal fixture recovers even with an app.quit() finisher — so the test guards the fixed pattern completing rather than red/green-reproducing the hang. | 2 个月前 | |
fix(realtime): robust findCard — hyphen/punct-tolerant, scored, disambiguated Voice/typed task lookup was brittle: findCard lowercased the title only (never normalized hyphens/punctuation), matched by contiguous substring, and took the first hit. So spoken "message visibility" never matched stored "message-visibility", truncated/reordered forms missed, and ambiguous refs silently mutated the wrong card (assign/update are immediate writes). - normMatch: strips ALL non-alphanumerics (hyphens included), kept separate from the confirm-word `norm` which must preserve token shape for echo-back. - scoreCard: tiered, order-independent, truncation- and prefix-tolerant scoring. - findCard now returns {ambiguous} when the top two are within AMBIGUOUS_MARGIN; execAssignTask/execUpdateTask ask "which one?" instead of guessing. - Matcher is identical to bin/find-task.cjs (validated --selftest 8/8); ports the same 8 cases into test/realtime-findcard.test.cjs (node, dependency-free). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 3 个月前 | |
fix(reflect): make summary parsing robust to literal model output - replace JSON-string-bound condensed output with a shared delimiter-framed contract - parse literal summary text deterministically with CRLF normalization and complete outer-fence handling - fail closed on partial, duplicate, out-of-order, unterminated, or malformed framed output - preserve valid legacy JSON responses as a backward-compatible fallback - add regressions for quotes, backslashes, Windows paths, multiline text, Markdown fences, JSON-like content, and malformed legacy JSON | 1 个月前 | |
fix(0.4.7-rc): ask-me ordering, release drop for fresh installs, compact-latch tests Three specified items for the 0.4.7-rc round, cut from release/0.4.6-rc so what runs under `npm run dev` is rc1-plus-these and nothing else. 1. ASK ME board ordering (askme-board-unordered) The board had NO comparator: `tasks.filter(waitsOnHuman)` rendered in whatever order cards happened to sit in a 440KB tasks.json, so a question from five minutes ago could land below one from three days ago. Sorts the OUTER card list by each card's newest still-open ask, descending. "Open" is derived from openQuestion() — the same predicate waitsOnHuman already calls — rather than defined a second time. Unparseable/missing askedAt sorts last and never throws. The humanQA array INSIDE a card is a conversation history rendered through the "VIEW N EARLIER ANSWERS" collapse and is left strictly chronological; a test asserts nothing reverses it. The ordering rule lives in a new import-free askMeOrder.ts so the comparator gets a real behavioural test; AskMeTab.tsx applies it. Testing it inside the .tsx would only have been possible as a source-string assertion, which cannot catch an inverted comparator. 2. Release drop reaches fresh installs (release-drop-reaches-everyone) VERIFIED, NOT REIMPLEMENTED: the 0.4.6 -> 0.4.7 half already works, because the stamp write is unconditional while the emit is gated, so any install that boots 0.4.6 once is armed. Untouched. The real defect: the drop required a `last-run-version` stamp to already exist, but the stamp and its reader shipped in the SAME release, so no earlier install had the file and a fresh install had nothing either. New pure shouldShowReleaseDrop(previous, current) in shared/updateState.ts: (a) fresh install -> fires (b) 0.4.6 -> 0.4.7 -> fires once (c) same version -> never (d) 0.4.7-rc.1 -> rc.2 -> fires, deliberately downgrade -> never (e) UpdateToast drop-block behaviour unchanged (d) is the isNewer edge, FIXED LOCALLY rather than declined: isNewer discards -rc.N, so the decision asks "is this a downgrade?" instead of "is this newer?". Doing it in the drop decision keeps prerelease semantics off the badge/pending state machine, which reads isNewer for other decisions and is frozen 0.4.6 behaviour under active rehearsal. Durability: the stamp write gets its own try/catch and the drop fires only when the stamp actually LANDED. An unwritable userData otherwise leaves `previous` null forever, which would reopen the drop on every single boot. 3. Compaction-latch tests (compact-latch-untested) #270's code is correct and is NOT refactored or touched. queueDelivery.ts has zero imports by design, so both primitives the latch rests on get real behavioural tests: canDeliverToAgent refuses an agent blocked on a human prompt, and deliverWithAcknowledgement reports false without acknowledging on a failed send. The two guards inside useHive.ts (the pre-enqueue gate, and the latch written only when `sent`) are pinned by source assertions, the house pattern for a hook whose import graph cannot load under node:test. All three guards were mutation-tested: inverting the comparator, restoring the old `previous &&` requirement, and dropping the `sent &&` latch gate each fail their tests. Full suite: 616 tests, 611 pass in-sandbox; the 4 failures and 1 timeout are all EPERM-on-$HOME / Electron-binary sandbox artifacts and pass 26/26 when re-run outside the sandbox. typecheck node + web clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
fix(release-drop): a loader inside the modal until the frame paints (b) Even with the render-blocking font fetch gone (a), there is still a window before the frame paints — remote images (img-src stays https:) can hold the iframe's load event — and in it the modal showed a bare paper rectangle that reads as broken, which is what makes a user reach for cancel. A loader now covers the frame until it is ready: the landing site's three square dots marching over warm paper, in the file's existing palette. It is revealed on the FIRST of two signals — the iframe element's own onLoad (it fires on the parent, needs no script rights in the sandboxed child) or a 2.5s timeout cap, whichever comes first. The cap matters because `load` waits for subresources: without it a drop with a slow image would hold the loader for the whole fetch, so we reveal the paper page and let images arrive into it rather than sit on a spinner. Reveal is monotonic — it latches true and never flips — so it cannot flicker. Built against the fails-open shape: the iframe is ALWAYS mounted at full opacity and the loader is a separate overlay that is removed on reveal, so a broken reveal degrades to a brief extra spinner, never a permanently hidden frame; and the timeout guarantees reveal fires even if onLoad never does. The tests pin the reveal wiring (onLoad AND the cap, iframe never gated on reveal), not just that a loader renders. No chrome button is added — dismissal stays Esc / close / backdrop, per the file's design. Tests mutation-checked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LDbmAwrgtGskFGCRkYZBx4 | 1 个月前 | |
fix(updater): make the notes parser survive rendered HTML Belt and braces behind the previous commit, which is the actual fix. This one helps nobody currently on an old version, because the toast is drawn by the already-running app. It exists so that a future release which forgets releaseInfo.releaseNotesFile degrades to something correct instead of to a CSS rule. The parser was only ever fed markdown, by every test in this file, while production fed it GitHub's rendered HTML from the atom feed. The tests were faithful to the wrong input, which is why a well-tested function shipped broken. Three changes, all narrow: - <style> and <script> blocks are dropped whole, before line splitting. A stylesheet is CSS on every line and one of those lines parses as a markdown bullet, which is the entire original bug. - <h1-6> counts as a heading and <li> as a bullet, so the what's-new section is found and ends at the next heading exactly as it does in markdown. - Apostrophe entities are decoded early, or `<h2>What's new</h2>` never matches the section marker and the digest leads with the page preamble instead. Deliberately not </>, which would manufacture tags out of escaped text. Mutation-checked. Reverting the whole change turns 3 of the new tests red; leaving the change in but skipping only the <style> strip turns 1 red, which also shows the two guards are independent rather than restating each other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 1 个月前 | |
fix: enable Electron renderer sandbox Keep Chromium's OS sandbox enabled for every BrowserWindow and pin the boundary with an AST-based regression test. | 2 个月前 | |
fix(updater): guard restart re-entry and report a failed quit Restart-to-install could get wedged with no feedback, which is the recurring "The command is disabled and cannot be executed" seen twice in updater.log (13 Aug and 22 Aug, six restart clicks before one install landed). Two causes, both fixed here: 1. Re-entry. The handler used to abort any in-flight restart and fire a fresh quitAndInstall on every click. The second quitAndInstall hits a native command Squirrel has already disabled by the first, which is exactly the disabled-command error. A guard now refuses a duplicate while one restart is pending. 2. Silent failure. quitAndInstall reports a refusal through the autoUpdater error event, not a throw, so the handler's await never settled and the button spun forever with no way back, which is what drove the repeated clicking. A failed quit now settles the pending restart (failPendingRestart) so the renderer reports the error and restores its notice instead of hanging. The pendingRestart resolver now carries an outcome so both the cancel path and the failure path report the truth. abortPendingRestart keeps its signature (index.ts app:cancelClose still calls it). Extends restart-cancel.test.cjs with the guard and settle-on-error contracts; all 5 assertions green, typecheck:node clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
fix: recreate the resume terminal only after spawnPty accepts the replacement (#575) restartWithModel's resume path ran disposeTerminal+acquireTerminal BEFORE the spawnPty call that can still fail, so a refused spawn (!res.ok, e.g. cwd gone or CLI missing) or a refused resume (res.resumed !== true, the main process's requireResume early-return) threw after the destroy: the pool kept the fresh empty xterm, the old one's scrollback was unrecoverable (node-pty keeps none), and the agent's action label stayed stuck at 'recreating terminal…' — one failed Restart & Continue erased the pane the button exists to redraw. The fix reorders: kill/reset stay pre-spawn, but the destroy/recreate/ generation-bump now runs after the two throw gates, only when the replacement process was actually accepted. The spawn answer beats the CLI's first frame, so startup output cannot be missed, and the remount's attach re-requests a PTY redraw for anything it raced. The Electron repro (test/repro/bug-14.*) becomes a proper regression test, test/restart-terminal-preserve.test.cjs: the failed-restart scenarios must keep the pooled entry, scrollback and label, while a successful restart must still recreate the terminal — only after the spawn answer. | 1 个月前 | |
Scope the localStorage roster fallback to the hive that wrote it localStorage is partitioned by origin, so every hive the app opens shares exactly one. When a hive has no roster.json yet, the store falls back to localStorage unconditionally — and that localStorage still holds the last hive's roster. Opening a brand-new config therefore rehydrated the previous workspace's agents, archived and restorable entries, each still carrying the cwd of the workspace it was hired in, and the seed flush then wrote them into the new hive's roster.json. Stamp the hive the keys were written for (cth.rosterHome) and read the fallback only when it matches. An unstamped localStorage is still adopted once, so installs upgrading into this keep their floor, and the dev <-> packaged origin bridge the mirror exists for is untouched: it runs through roster.json, not through this fallback. Closes #236 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(roster): keep the empty-write guard armed after a refusal The first-empty-write guard set `wrote = true` when it DECLINED a write, so a renderer reload that re-sent the same empty snapshot got through on the second attempt and flattened a non-empty roster (seen live: declined backup at 20:38:47, file flattened two minutes later). The guard now only disarms when a non-empty write lands — proof the renderer actually holds a roster — so a later empty write is a genuine deletion. Adds a regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> | 2 个月前 | |
feat(settings): one Save button, and the Connections tab stops repeating itself A1 + A2, in one pass over SettingsModal.tsx so the file is touched once. A1 — ONE SAVE BUTTON Settings persisted three different ways at once. Toggles wrote to disk the instant you clicked them, Autonomy & Budgets had its own Save, and max-turns saved on blur. Nothing on screen told you which kind you were looking at, so "did that stick?" had no answer a user could learn once and reuse. Every setting that goes through updateConfig is now STAGED and written by the footer Save, in a single call. That call is the only updateConfig left in the file, and a test holds it at one: a partially-applied save is no longer a state the app can reach. Closing with staged changes now asks first. That guard only fires for controls that USED to persist on click, because those are the ones whose behaviour changed; the text fields always needed a Save. Two exceptions stay immediate, on purpose, and neither is a setting: * API keys go to the write-only secret broker. Nothing can read one back to diff it, so there is no staged value to hold. AiEnginesSettings keeps its per-provider save. * Free Flow arms a global hotkey in main. Staging it would leave the hotkey and the checkbox disagreeing until someone pressed Save. Slack and webhooks keep their own controls too: they connect and disconnect live rather than storing a preference. Auto-compact needed care. It lives inside the missions array, so instead of staging a whole array captured at click time, saveAll re-reads config and edits only that mission — otherwise saving would silently revert a different mission that main or another window had changed meanwhile. A2 — CONNECTIONS Nothing is deleted; the founder was explicit about that, and a test now says so. With the MCP directory redesign still parked in A3, what was left here was consistency, and there was real drift to fix: the small-caps section heading was written out inline SEVENTEEN times in three slightly different forms, and the 2px section rule four times. Both are now defined once. That is how a tab ends up looking subtly unlike its neighbours, and it is now impossible. Verified by mutation: pointing one toggle back at updateConfig fails 2 of 10, bypassing the close guard fails 1, re-inlining a heading fails 1. Suite 664/664 outside the sandbox, both typechecks clean, renderer builds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 1 个月前 | |
fix(skills): resolve the SKILL.md directory before installing a repo-root source A catalog entry pointing at a repository ROOT was treated as "the root is the skill" unconditionally: walk('') recursed over the entire repository, and MAX_FILES / MAX_TOTAL_BYTES — meant to bound one skill folder — were applied to the whole repo. Any repo keeping its skill under skills/<name> failed with "that skill is larger than this installer will fetch" even when the skill itself was a few KB (issue #385). resolveRepoRootSkillDir() now runs first for root sources: root SKILL.md keeps today's behavior for the well-formed entries; otherwise skills/<name>, .claude/skills/<name> and <name> are probed — each only when its first segment exists as a directory in the root listing, so no blind 404s — and a repo with no SKILL.md anywhere is refused with a structured "install it by hand" error before a single file downloads. Tree URLs keep their explicit path, untouched. The lister is injected so the policy is unit-tested without the network. Closes #385 | 1 个月前 | |
feat: start the hive on first run, and add Skills, Prerequisites and release drops THE BOOTSTRAP BUG. `bootstrapHiveServices()` runs once at app-ready and opens with `if (!hive.enabled()) return` — and a fresh install has `harnessHome: null` at that moment. Onboarding then sets it through `config:update`, which did not re-bootstrap. So on every first run the message router (the poll loop draining outbox/ into inbox/), the hook server, the telemetry collector and the mission scheduler stayed dead for the entire session: mail never moved, cards never reported, "Restart & Continue" had no session to resume, and nothing said so. It healed on the next launch, which is exactly why it survived. changeHome had always handled this by relaunching; onboarding does not. Now bootstrapped on the null -> set transition, with the live session id also recorded from the usage tick so continuing works even when a hook never lands. SKILLS. What every agent can already do across Claude Code, OpenCode and Codex, deduped project > user > bundled (the precedence the CLIs themselves apply), plus a browsable catalog parsed from a README with no machine index. Install treats the source as hostile because it is arbitrary content from a public list: sanitised destination names, containment re-checked AFTER path resolution, 60-file/2MB/depth-5 caps, symlink entries skipped, and nothing written until the whole tree resolves. Uninstall refuses any path it cannot prove is a SKILL.md folder inside a managed root, and never touches bundled skills. PREREQUISITES. One Settings page for the tools the app leans on — uv, git, Node, MemPalace, every engine — with live status, the real path when present, and platform-correct commands when not. The button delegates to Michael rather than shelling out, so a real confirmation step sits in front of anything that writes outside the app. RELEASE DROPS. A release body can carry an authored HTML page between <!-- drop --> markers, shown as a centered modal. It renders in an iframe with sandbox="" AND its own default-src 'none' CSP — two independent controls, either sufficient — because the renderer it would otherwise reach has window.cth bridged onto it, where a script is arbitrary code execution with the app's authority. SETTINGS HERO CARD. Version, plan, sponsor and a way to reopen the notes, with contents fetched from docs/hero.json so they change without a build. Remote DATA, never markup: every field is a React text node, URLs must be https, strings are length-capped, and a malformed payload degrades to the compiled-in defaults. Also: OpenCode no longer preselects a BYOK model the user may not own and then silently fall back, and the voice button explains how to enable dictation (including that Groq is free) instead of sitting dead. 31 new tests, 239 total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix(slack): Stop now persists slackEnabled:false so boot does not re-arm Stop tore down the running server but never wrote the flag. Boot re-arms from `slackEnabled && slackSigningSecret`, so quitting after an explicit Stop brought Slack straight back on the next launch, and the Settings pill kept reading ON with the token fields open until the modal was reopened. Start already persists the flag (from the renderer, in `startSlack`), so this is the missing half of that pair, and the two functions now read as inverses. Persist BEFORE tearing down, which is the one deviation from the fix suggested in the issue. If the write throws — read-only volume, ENOSPC — the server is still up and the UI stays truthful. Stopping first and failing the write leaves a dead server that still reads as Connected with the flag set, which is this same bug again with nothing to show for it. Only the user-initiated `slack:stop` handler clears the flag. changeHome, quit and reset call `stopSlackServer()` directly and must not, as the issue calls out: they are lifecycle, not a user turning the integration off. The new test asserts both halves — index.ts imports electron and cannot be require()d, so it reads the source, the same way test/update-applied.test.cjs does for updater.ts. It anchors on the three teardown lines rather than counting occurrences in the file, so that a teardown which disables Slack via a helper is still caught and a legitimate second clear elsewhere is not a false failure. | 1 个月前 | |
fix: add socket timeouts to postSlackReply and downloadSlackFile postSlackReply (src/main/slack.ts) and downloadSlackFile (src/main/index.ts) issued raw node:https requests with no timeout anywhere. A peer that accepts the connection but never responds leaves the promise pending forever, which wedged the done-summary poller (its finally never ran, so slackDonePolling stayed true for the process lifetime), hung the loopback /reply handler, and silently dropped inbound file messages that had already been 200-acked — the poller's designed transient-retry path never ran because a stall produces no error at all. Arm req.setTimeout (12s, matching fetchText.ts) that destroys the request so the existing 'error' handlers settle the promises through the usual transient-failure path: postSlackReply resolves ok:false (poller retries), downloadSlackFile resolves null (attachment dropped, message still forwarded). Regression coverage: test/slack-timeout.test.cjs drives the real postSlackReply against a stalled local TLS endpoint (fails on unfixed code) and pins the download timeout by source check; the full end-to-end stall scenario (poller wedge, /reply hang, acked-then-dropped message) lives in test/repro/bug-12.repro.cjs. | 1 个月前 | |
fix(slack): dedupe one message delivered as both app_mention + message.* When the Slack app subscribes to BOTH the mention path (app_mention) and channel-message events (message.channels/groups), a single @-mention in a channel fires BOTH events. They arrive as two separate event_callback deliveries that share channel+ts but each carry a DISTINCT outer event_id. With no dedup, shouldTrigger() returned true for both, so onMessage() fired twice — enqueueing the request to god twice AND posting the ":hourglass: Received" ack into the thread TWICE. Fix: a bounded-FIFO idempotency cache (SeenEvents, cap 500) keyed on the message identity that is shared across both event types: `channel:ts`. NOT event_id (differs per delivery → would miss this double) and NOT client_msg_id (not guaranteed on app_mention). The same key also absorbs Slack's retry of an un-acked event. The check is gated AFTER the existing mention/activated-thread filter, so non-triggering messages are unaffected and behavior is otherwise unchanged. - src/main/slack-trigger.cjs: add SeenEvents bounded-FIFO class + dedupKey(ev) (pure JS, same testable pattern as ActivatedThreads); export both + SEEN_EVENTS_MAX. - src/main/slack.ts: hold a per-server SeenEvents; skip onMessage on a repeat key. - test/slack.test.cjs: +7 tests — identical key across event types, missing channel/ts uncacheable, first-new/repeat-duplicate, empty keys never deduped, bounded FIFO eviction, and two integration tests proving the dual-subscription double-fire collapses to ONE delivery while a genuinely new message still fires. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> | 4 个月前 | |
fix(hive): inject standing goals only when changed | 1 个月前 | |
fix(hive): stop a single task edit from wiping every card on the board Answering a question on ASK ME, or moving a card in the task detail, silently destroyed data across the WHOLE ledger. Live damage: two cards were reduced to the fields the renderer happens to model, taking `result` — the verbatim Slack reply text posted back to the user — plus repo/scope/origin/deliverable/commit with them. A hand-written `priority: "high"` came back as the number 3, and a card spelling its dependencies `deps` gained a phantom `dependsOn: []`. Two independent faults, and each needs its own fix: 1. `hive.writeTasks` replaced tasks.json wholesale. Every caller holds a PARTIAL model of a card — the kanban parser knows nine fields, the god writes as many as the work needs — so one edit deleted every unmodelled field on every card. It now merges the incoming cards over the on-disk ones by id. Deleting a card still works: the incoming list is the card MEMBERSHIP, so a card dropped from it (kanban dismiss, voice delete_task) is gone. Merging protects fields only. 2. The merge cannot save `priority` or `deps`, because `TaskDetailOverlay.move` wrote back the display-parsed model and a coerced value is a real value that wins the merge. It now patches the raw ledger entry — the same pattern `TasksKanban.dismissTask` already used — so the coercion is never produced. AskMeTab moves to the same raw-ledger patch. Its parse was already a passthrough so it was not the field-stripper, but writing its 5s-old snapshot back as the card membership could delete a card the god added between polls, and it could answer a question the god had swapped in underneath the user. The shared rules live in src/shared/taskLedger.ts so main and the renderer cannot drift apart on them. | 2 个月前 | |
fix: reset usage when a PTY exits Tearing down a PTY forgets the agent's circuit-breaker state but leaves its telemetry sessions in place. Because live usage is summed across every retained session for an agent id, a replacement agent reusing that id starts with its dead predecessor's tokens already counted against it, and a per-agent token cap can trip on spend that belongs to a session that no longer exists. Forget the agent's live telemetry sessions alongside the breaker state, so a replacement starts from its own usage. Only the in-memory live accumulation is cleared; the on-disk cost ledger and the transcript fallback are untouched, so lifetime spending is still reported. | 2 个月前 | |
feat(telemetry): instrument the activation funnel (launch → first agent) The 6 existing events are fine; the blind spot is coverage. There is zero instrumentation between `app_launched` and `agent_spawned`, which is exactly where activation happens and where people fall out, and a failed spawn sends nothing — so Mode 2 (an engine CLI with no unattended install path) and "the user changed their mind" are the same non-event. Founder brief: the most detailed non-content metrics we can get, with the policy moved to match the code in the SAME change. Five new events, all non-content and closed-enum, wired at the four spawn-funnel sites in spawnAgentCore + the install-PTY exit handler + the onboarding transition: onboarding_completed { provider } agent_spawn_attempted { provider } (gated so the install relaunch never double-counts an attempt) agent_spawn_failed { provider, reason } reason: cli_missing | cwd_missing | already_running | spawn_error agent_install_started { provider, rung } rung: npm | node-then-npm | native agent_install_finished { provider, rung, outcome } outcome: agent_launched | install_failed The funnel now reads end to end (app_launched → onboarding_completed → agent_spawn_attempted → {spawned | failed | install_started → install_finished}), so (attempted − spawned) is the activation fallout and agent_install_finished is the "the auto-installer could not complete" signal that used to be silent. Docs move with the code (the #238 lesson: change the promise, then collect): TELEMETRY.md and docs/privacy.html both gain the five events, and privacy.html also picks up update_applied, which it was missing — closing a pre-existing doc/code drift so the page mirrors the allowlist exactly. Anonymity is unchanged: every added property is a closed CLI name or a fixed enum, the hard allowlist in analytics.ts drops anything else, and the events stay `$process_person_profile: false`. No content (prompts, output, paths, repo names, hostnames) is added — that decision is the founder's and is raised separately, not taken here. Target 0.4.7. 0.4.6 stays frozen; this touches nothing in it and does not touch the updater. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
feat(telemetry): count human-sent messages, closing the activation funnel The funnel currently ends at agent_spawned, so an agent that was started and then never spoken to is indistinguishable from one in daily use. This adds `message_sent`: a COUNT of the messages a person sends to an agent, and nothing else. No content, in any shape. No text, no length, no character count, no first-N-characters, no hash of the body. The event has exactly one property, `surface`, and it is a closed enum. The IPC channel the renderer uses to report a message does not accept a message argument at all, so there is no shape in which the body could cross it by mistake. The separate question of whether content telemetry should ever exist is untouched and remains open. Counted at the SUBMIT, not the keystroke. `pty:write` fires on every character typed; instrumenting it would have produced a keystroke meter orders of magnitude above Ryan's 7.47-events-per-active-install-per-day baseline. The four surfaces are counted where a person actually sends: - terminal — terminalPool's submit boundary, once per input chunk. A bracketed paste is excluded, so a five-line paste is one message (the Enter that sends it) rather than six. - composer — inside MessageQueueComposer.queueIt, NOT inside the shared enqueueMessage store action, which also carries work orders, Slack inbound, nudges and compact commands. - steer — at the control:steer IPC handler, not inside control.steer(): closingTime and the voice action layer call that directly and neither is a person typing. - hive — at hive:send, gated on from === 'human', so agent-to-agent traffic is never counted. IME is inherited, not restated. The two renderer surfaces sit past the existing isComposingKey guard, so an Enter that picks a CJK candidate is not a submit and cannot inflate the count for the users the i18n work was for. The renderer may name only `terminal` and `composer`. `steer` and `hive` are counted in main at handlers that already receive them, so letting the renderer name those two would open a double-count. The event name is fixed main-side: the renderer chooses a surface, never an event. TELEMETRY.md is updated in the same commit, and a new test asserts that the allowlist in analytics.ts and the doc's event table name exactly the same events — so a third contradiction between published policy and behaviour fails the build instead of shipping. Tests cover the fails-open shape, not just the function: the IPC channel literal on both sides of the bridge, the surface literal at each call site, and the doc/code lockstep. Twelve mutations were checked and all twelve go red, each on its own test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LDbmAwrgtGskFGCRkYZBx4 | 1 个月前 | |
fix(telemetry): scope the transcript usage fallback to the agent's own session (D11) TelemetryCollector.getAgentUsage() falls back to transcriptFallback() whenever an agent has no live OTel data yet — the common case right after spawn. That fallback called readAgentUsage(cwd) with no session filter, so it summed every .jsonl transcript ever left in that cwd's Claude project directory: every other agent's history and every past run's, not just this agent's own. cwd is routinely shared — several spawn paths skip git isolation outright, and even isolated workers can share the base repo's cwd — so this directory accumulates fast. Verified on disk against this very repo's shared project dir (~/.claude/projects/-Users-echan-Code-munder-difflin): the unfiltered sum across 4 sessions is already 65.7M tokens (mostly cacheRead, since every turn re-reads the whole context), while each individual session's own filtered total is a few million at most — the same shape as the reported incident (a probe worker with zero LLM calls reaped citing 143,369,766 tokens; an equivalent worker's real usage, per fleet.json, was 251,901). Fix: transcriptFallback now resolves the agent's current Claude Code session id (resolveSessionId, wired to hive.lastSession in index.ts) and passes it as readAgentUsage's existing sessionId filter. When no session id is known yet for that agent — the true zero-usage instant right after spawn, before any hook has fired — it reports no data instead of summing someone else's history. Confirmed the id spaces actually line up: for every transcript file with real assistant turns, 100% of records' in-file sessionId matched the filename (one file had 0% because that session genuinely never got a model response — a known-inert worker, not a counterexample). The returned sample keeps sessionId as '' regardless of the resolved id, matching the existing #56 cost-ledger dedup guard in index.ts (`if (sample?.sessionId) appendCostLedger(...)`) — the resolved id is used only to filter the read, never surfaced as if it were a live OTel session. This also explains the earlier breaker message storm (repeated steer -> constrain -> steer -> constrain against non-monotonic totals): the level only re-escalates when a trip recovers and re-trips, and the wild swings in the reported token counts are consistent with getAgentUsage flipping between a small live aggregate and a huge unfiltered historical sum beat to beat. With the fallback scoped, samples become non-decreasing except for a single downward step when a resumed agent's in-memory aggregate goes null -> non-null (aggregateLive starts at zero for this app run, while the transcript carried the whole prior session) — a one-time step, not flapping, so no separate latch-the-trip mechanism was added on top. Deliberately did NOT redefine breaker.ts's tokensOf (input+output+cacheRead +cacheCreation) to an output-only or windowed measure, though it was the other half of the suggested fix shape. CommandCenterPanel.tsx:595 independently recomputes that same all-4-field sum from the same AgentUsageSample to drive the per-agent token meter against the same cap; redefining what the breaker compares without touching the meter would desync the visual "N% of cap" display from what actually trips the breaker. The traced root cause was the fallback's cross-session summation, not the 4-field formula itself, so this fix stays scoped to that. Also left alone (found while tracing callers of readAgentUsage, still summing unfiltered and still capable of showing an inflated number, but neither is live/consumed today so neither affects breaker behavior or today's verification): StubUsageProvider (src/main/usage.ts) is dead code, never instantiated since telemetry.ts replaced it as the live UsageProvider; the `hive:agentUsage` IPC handler (index.ts) and its preload binding are wired but called by no renderer component. Also left alone: the D11 card's UX nuance that a worker tokenCap set below its own boot baseline should fail loudly with a real number rather than just not insta-killing it — out of scope for this fix. One more consumer now returns null more often than before: `telemetry:usage` (index.ts, renderer-facing) will read null for an agent with no live OTel and no known session yet, instead of a wrong-but-populated number. Confirmed the other consumers handle it correctly unchanged: runBreakerBeat (tokensOf(null) = 0), the velocity arm (requires both samples present), the cost ledger (already gated on sample?.sessionId), and workerTokensUsed (`s ? ... : 0`). Regression test: test/telemetry-session-fallback.test.cjs, registered in package.json's test:focused. Reproduces the bug directly — fails against the pre-fix code with the exact shape of the incident (a fabricated foreign session's ~1,000,000 tokens attributed to an unrelated agent) and passes with the fix. npm run typecheck and npm run test:focused (159/159) both green. | 2 个月前 | |
fix(queue): one gate for every automatic writer, and stop guessing at the prompt The message queue kept breaking because two different loops each decided for themselves when it was safe to type into an agent's terminal, and both decided it from a model of the input line built by counting keystrokes. Route the inbox nudge through the queue. It was the last automatic writer typing straight into a terminal, which made it the one that could land on top of whatever the user was mid-way through writing: its text fused onto the half- written line and the pair was submitted as one garbled prompt. It now enqueues like /compact already did, so effect #4 owns every decision about when a terminal may be typed into — idle, off cooldown, past boot grace, delivery not paused, no user draft in the way. One gate, one place. Read the prompt instead of modelling it. `inputDirty` is inferred from keystrokes, and a TUI that swallows keys for its own UI leaves it set while the prompt is visibly empty. Nothing corrected that, so the queue stayed blocked by a draft that did not exist — the "messages never arrive" bug. xterm already holds the rendered screen, so read the cursor row and strip the box-drawing and prompt chrome. Used only to clear a phantom, never to invent a draft: being wrong the other way would type over something the user really did write. Never destroy or close what the user owns. A draft or picker block now lasts half an hour rather than a minute, and when it does expire delivery simply types after whatever is there. The Ctrl-U wipe (mine, this session) and the automatic Escape at a picker are gone — deleting text is worse than garbling one prompt, and we cannot see whether Escape closed anything. Both keys remain on the composer's own button, where the user asked for them. Show it. A held queue looked identical to an idle agent with nothing to do. Agent cards and the fullscreen roster now badge `typing` when the user has unsent text on that agent's prompt, derived at render so the pty parser cannot overwrite it. Also: `updateAgent` persists when a durable field changes, so a model or command change survives a reload instead of living only in memory (Codex P2). 36/36 tests pass, typecheck and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DUpadJJuEQz1zpQ6Vk83j3 | 2 个月前 | |
fix(terminal): Shift+Enter inserts a newline instead of submitting xterm.js encodes Shift+Enter exactly like Enter — a bare CR — so a TUI cannot tell the two apart. At a Claude Code prompt inside an agent's terminal pane, Shift+Enter therefore submits the line instead of opening a new one, and writing a multi-line prompt by hand means typing a trailing backslash. `/terminal-setup` installs the right mapping for iTerm2 and VS Code, but there is no way to apply it to our pane, so the pane has to send the sequence itself. Shift+Enter now writes ESC CR, the sequence /terminal-setup configures. This is provider-neutral: it is the same sequence Alt+Enter has always produced, so any TUI that honours one honours the other. The mapping is deliberately narrow, and lives in its own structural module so it is unit-testable without xterm or a window. Plain Enter still submits. Alt+Enter is untouched, since it already produces this sequence natively and intercepting it would double-encode. Ctrl+Enter and Cmd+Enter stay with the TUI and the app's own shortcuts. Only keydown answers — xterm's custom-key hook also sees keypress and keyup, and answering twice would insert two newlines. The check sits BEFORE the existing Ctrl/Cmd gate in the handler, because Shift+Enter holds neither modifier and was returning early into xterm's default. Writing to the pty is the path the OSC colour replies alongside it already use, and is skipped once the pty has exited. Five tests in test/terminal-keys.test.cjs. Closes #481 | 1 个月前 | |
fix(terminal): https URLs in agent output are not clickable The link provider only ever looked for path tokens, and PATH_TOKEN_RE carries an optional Windows-drive prefix `(?:[A-Za-z]:[\\/])?`. That group matches the `s:/` inside `https://`, so a URL was claimed as an absolute path on drive `s:`, resolved against the agent's cwd, stat'd, missed, and silently dropped. The underline never appeared and the click did nothing. - terminalPaths.ts: urlTokenMatcher / stripUrlToken, and terminalLinkSpans(), which resolves URL spans first and drops any path token overlapping one. The ordering is the fix; putting it here rather than in the provider is what makes it testable, and this module is already where "what each type does" lives. - terminalPool.ts: provideLinks becomes a thin adapter over that. URL activation is Cmd/Ctrl-click only, matching the path links, and goes through the existing app:openExternal — no new IPC surface. https only, deliberately: the main-process opener accepts nothing else, so underlining an http:// would promise a click that cannot work. The scheme is lowercased on the way out because that opener matches `^https://` case-sensitively, and a terminal that printed HTTPS:// hit the same drive-letter path as the original bug. Balanced brackets stay in the URL — eating the `)` off a Wikipedia link lands the user on a 404 with nothing to explain it — and a bare scheme with nothing after it is not offered at all. The new activation test is a source scan because terminalPool.ts imports xterm and cannot be required; it pins both modifier gates and that openExternal has exactly one call site, so a second path around the gate fails the suite. | 1 个月前 | |
fix(terminal): https URLs in agent output are not clickable The link provider only ever looked for path tokens, and PATH_TOKEN_RE carries an optional Windows-drive prefix `(?:[A-Za-z]:[\\/])?`. That group matches the `s:/` inside `https://`, so a URL was claimed as an absolute path on drive `s:`, resolved against the agent's cwd, stat'd, missed, and silently dropped. The underline never appeared and the click did nothing. - terminalPaths.ts: urlTokenMatcher / stripUrlToken, and terminalLinkSpans(), which resolves URL spans first and drops any path token overlapping one. The ordering is the fix; putting it here rather than in the provider is what makes it testable, and this module is already where "what each type does" lives. - terminalPool.ts: provideLinks becomes a thin adapter over that. URL activation is Cmd/Ctrl-click only, matching the path links, and goes through the existing app:openExternal — no new IPC surface. https only, deliberately: the main-process opener accepts nothing else, so underlining an http:// would promise a click that cannot work. The scheme is lowercased on the way out because that opener matches `^https://` case-sensitively, and a terminal that printed HTTPS:// hit the same drive-letter path as the original bug. Balanced brackets stay in the URL — eating the `)` off a Wikipedia link lands the user on a 404 with nothing to explain it — and a bare scheme with nothing after it is not offered at all. The new activation test is a source scan because terminalPool.ts imports xterm and cannot be required; it pins both modifier gates and that openExternal has exactly one call site, so a second path around the gate fails the suite. | 1 个月前 | |
fix(terminal): recreate xterm on resume | 2 个月前 | |
fix(terminal): strip the CLI's quote rail from copied selections Copying a line of agent output pasted a stray block glyph with it: ▎ Munder Difflin — clones for you and your team, working 24/7. Claude Code paints a markdown blockquote INTO the character grid — the `▎` rail is a real cell in column 0, not a border drawn beside the text — so a triple-click or full-width drag selects it along with the prose, and xterm's getSelection() hands it straight to the clipboard. Sanitize on the way out, in the one place every copy path already funnels through (Ctrl/Cmd+C, Ctrl/Cmd+Shift+C, right-click). Per line: drop a leading run of gutter rails, keep the indent. Kept deliberately narrow so it can't corrupt legitimate copies: - a rail only counts when a space follows it or it ends the line, so bar charts and block art (`▌▌▌▌ 40%`) keep their bars; - only the left/right BLOCK elements are in the set. Box-drawing verticals (`│`, `┃`) are not — tree, --graph and framed tables indent continuation lines with `│ `, and stripping that would break far more than it fixes; - selections with no rail anywhere return byte-for-byte unchanged. A rail-only selection sanitizes to empty but still counts as a copy, so right-click doesn't fall through and paste into the agent's prompt. 10 tests, mutation-checked both ways: 4 fail against a no-op sanitizer, 3 (the guards) fail against a naive strip that also eats box drawing. test:focused 140/140, typecheck clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
fix(terminal): tell running TUIs when the app theme flips Founder found that switching light to dark left the TUI's own colours behind. Answering the OSC colour query fixed STARTUP only. A program that painted its panels from that answer keeps them until something makes it redraw: xterm repaints its own cells, and everything the program coloured explicitly does not, so the app and the TUI disagreed until the agent restarted. DEC private mode 2031 is the protocol for this. A program enables it to ask to be told, and the terminal answers CSI ? 997 ; 1 n for dark or ; 2 n for light. Confirmed against the shipped OpenCode binary, which enables 2031 and listens for both notifications, so this is what it was already waiting for. We now track which programs enabled 2031 and notify only those, since a program that did not ask would receive this as unsolicited bytes on its input. The notification goes to every pooled terminal rather than the visible one, so a background agent is not stale when you switch to it. Sent from the theme toggle itself rather than from the terminal view's theme effect: that effect also runs on mount, which would have fired a spurious notification every time you switched agents. | 2 个月前 | |
test(windows): make platform fixtures deterministic (#403) | 1 个月前 | |
test(usage): assert every field on the filtered path, not just output tokens The existing `sessionId filter sums only that session` test already covers two sessions in one file with both filtered and unfiltered totals, but it asserts `outputTokens` alone. Mutation-testing the suite against this branch showed what that leaves open: dropping `estimatedCostUsd` from the session bucket while still adding it to the unfiltered total, swapping cacheRead and cacheWrite, or skipping `model` on the bucket all pass 10/10. Those are exactly the numbers the cost ledger and the per-agent token cap read, and a short per-agent figure has nothing to contradict it. Two sessions in one file, distinct models and distinct cache read/write counts so a swap between them cannot cancel out. Cost is asserted relationally (each session priced, the two summing to the unfiltered total) so it pins the split without hard-coding a price table that will drift. A second test pins the sessionless-record equivalence: no sessionId means the record counts toward the unfiltered total and toward no filtered one - the same answer the pre-cache code gave by skipping it on a filtered query. Also documents what bounds perSession. Claude Code writes one transcript per session, named for that session id; measured across 2172 top-level transcripts, every one holds exactly one distinct sessionId matching its filename stem. The refresh-path clone is therefore O(1), not O(sessions-ever-seen). All three mutations now fail the suite; 12/12 green, both tsconfig targets typecheck clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
test: kill two suite flakes, a shared temp path and an unconditional proxy assert Both were intermittent failures with no owner, and neither is a product bug. update-applied used one fixed name in the machine temp dir for its unwritable-state-dir fixture. Two suite runs on a machine race: the second deletes the sentinel file in its own cleanup, analytics then recreates the path as a real state directory via mkdirSync(recursive), and the first run's non-recursive rmSync throws EISDIR. The directory outlives the run, so from the first collision onward writeFileSync throws EISDIR on every later run for everyone on that machine until a human deletes it. Flake once, then a trapdoor. Now a per-run mkdtemp name, and cleanup removes the whole directory. The crush tests asserted the proxied branch unconditionally. hive.ts gates installCrushConfig behind `if (port > 0)` and degrades on purpose when the hive-proxy sidecar does not report a bound loopback port, logging "proxy bridge ... did not bind". That degradation is correct product behaviour, so the tests were reading a crush.json production had chosen not to write and failing with ENOENT. They now assert whichever branch was actually taken: the full proxied contract when the bridge bound, and when it did not, that routing was left completely untouched rather than half applied. Coverage goes up and the degraded path gains an assertion it never had. Verified by forcing the no-bind path: identical ENOENT before, green after, and green again unforced. Product code is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tjbh3gAgKrkGetDWSX5SCt | 2 个月前 | |
test: update-applied asserts the $ip key #238 now sends #238 sets `$ip: null` on every event so PostHog cannot fill it from the connection. This test pins the exact property set an update-applied event carries ("nothing from the log itself rode along") and predates that key, so it fails on origin/main the moment #238 lands — not a merge interaction. The shipped behaviour is correct and unchanged; only the assertion was stale. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7eUT4Mh1EZ7rkJ2DiDens | 2 个月前 | |
feat(updater): acknowledge a successful check on the badge The founder's actual complaint: he clicks the version badge and nothing happens. It is working. On the latest release a manual check succeeds, finds no update, and the badge settles back to a quiet grey "latest" chip with no toast, no motion, no confirmation. A check that worked perfectly and a click that never registered are indistinguishable, so a working button reads as broken. The through-line across this whole bug was silence on success. This makes success speak: after a MANUAL check that finds no update, the badge flashes a brief positive acknowledgement ("You are on the latest version. vX is the newest release. Checked just now.") that auto-dismisses after a few seconds. It fires only for the manual no-update result, read back from the settled status; an available update is already loud on its own (the chip changes), and background 6h checks stay silent as before. Deliberately narrow: it does not touch the available/download/restart paths, which cannot be exercised until there is a real update to find (the 0.4.6-rc). Make success visible first, let the rc show the rest. Source-string tests pin the wiring (no jsdom harness in this repo); typecheck:web clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
fix(updater): hard-cap the update check so the badge cannot spin forever The founder's live 0.4.5 shows the toolbar badge stuck on "checking…". Root cause: runCheck awaits electron-updater's checkForUpdates() with no timeout. If the feed request opens but never responds (a stalled connection, a captive portal, a half-open socket after sleep), that promise never settles, so runCheck never leaves the "checking" state it emitted, the badge spins forever, and nothing is written to updater.log. electron-updater caches its in-flight check promise, so once one check hangs, every later check returns the same hung promise and the native updater is wedged until the process restarts. A spinner that never resolves is worse than an error: it looks like the app is working when it is not, and it hides the fix, because a wedged check will not detect 0.4.6 either. runCheck now races the check against CHECK_TIMEOUT_MS (30s; the feed is a few-hundred-byte YAML, so anything past this is hung, not slow). A timeout falls through to the existing catch, which logs it, shows an error state, and runs the notify-only fallback poll, so the badge always reaches a terminal state and the failure is finally visible in the log. Source-string tests in update-check-timeout.test.cjs pin the cap; typecheck:node clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
release: land the 0.4.5 drop and Dwight's manual-update fixes Drop: docs/drops/v0.4.5-release.md, taken as a single file from e8329bf on release/v0.5.0-notes. That branch stays unmerged because it carries Pro, which left the drop for the release we ARE shipping stranded on the one we are not. Nothing else came across. From dcfb216, minus its ReleaseDrop hunk: the manual-update path now resolves the installer for this platform and arch out of the release assets and offers that instead of a page to go hunting on. main's openRelease guard already admits it — download URLs live under the same github.com/REPO/ prefix. From 1b28c54, its UpdateToast hunk only: restart closes the notice before asking main to quit, so the kill-and-quit warning is the only question on screen, and the notice comes back if main reports it could not quit. Two of Dwight's hunks deliberately left behind. His ReleaseDrop "Download X.Y.Z" button, because the founder has since ruled that the drop carries no buttons at all. And his QuitWarningModal z-index, because he found the same bug I did and the branch already fixes it at 1000 rather than 600. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018R7wTroJDW1MbHC8Qi9eaY | 2 个月前 | |
feat(updater): the badge does the real auto-update, not just a manual download (Option B) The founder's actual complaint was that clicking the badge "does not update" — because the badge only ever offered a manual download (a DMG and a drag-to-Applications card), never the native download/restart the Settings pane runs. When 0.4.6 lands he would click, get a DMG, and rightly say auto-update still does not work. Now, when the native updater has staged an update, the badge drives the same path Settings does: - `available` -> action `download` ("vX · update"), kick the native download - `downloaded` -> action `restart` ("vX · restart"), quit and install Manual download is demoted to the notify-only fallback only (`available-manual`, where the native updater could not fetch it); the hover card with drag-to-Applications steps now shows only in that case. The success acknowledgement from this branch is unchanged. This is the LAST code change before the 0.4.6 freeze, and it is the thing the rc rehearsal is meant to exercise: rehearsing without it would test the old badge behaviour, a build that never ships. update-state tests updated (available -> download, downloaded -> restart); all 17 green, badge acknowledgement tests green, typecheck:web clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dMe8Mm2eas1SwvUiu3XXr | 2 个月前 | |
test(bench): make the benchmark workload env-configurable The PR body documents BENCH_AGENTS / BENCH_ROUNDS, but the committed script hard-coded 10 and 30 and read neither — the recorded 60-agent / 150-round demo runs used a local edit that never made it into a commit. An unparseable or non-positive value exits non-zero rather than falling back to the default: a benchmark that quietly ignores the size you asked for reports a number for the wrong workload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 1 个月前 | |
feat(voice): redacted read path to hive messages (get_messages) Realtime Michael gains a read/brief-only get_messages tool over hive inbox/outbox message content (full message by id, one mailbox, or latest across the floor). All secret redaction runs MAIN-side (voiceMessages() -> redactSecrets()): the renderer/ voice layer only ever receives already-redacted bodies and holds zero redaction policy; the BYOK OpenAI key never crosses IPC (preload stays boolean-only); no new write path -- voice writes still go through the confirm-gated action spine. Redaction covers provider keys (sk-/sk-ant-), Slack, GitHub, AWS (incl. labeled aws_secret_access_key), Google, JWT, PEM private-key blocks, and Bearer/sensitive key=value. 33/33 voice-messages tests; tsc clean. Implemented by the hive (Kevin), security-reviewed by Pam. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GnThbJQvKg1FEdzCcWMcp | 3 个月前 | |
feat(triggers): one home for everything that starts work without you Promotes the God orchestrator's "Schedules" sidebar tab to "Triggers", a hub for the four ways work can begin without a human typing — schedules, context, webhooks, and organization — plus a conditional Trigger History tab. Each trigger's behaviour becomes explicit and editable instead of hardcoded. Reading the compaction path closely to build the Context trigger turned up three bugs: - The context-pressure gate was documented in config.ts but never implemented. Every live agent was compacted on every tick, hourly, regardless of context fill. The gate is real now. - Seven of eleven providers never compacted at all: the command lookup was a switch whose `default:` returned null, and the caller skips null. It is now a total Record so the compiler stops the next person who adds a provider. - `/clear` was typed at every provider regardless of meaning. Grok, OpenCode and Pi use `/new`; Qwen needs `/compress`; Crush's equivalents are palette rows, not slash commands; Copilot runs in print mode and cannot receive one. Each entry in the new table cites the shipped binary it was verified against. Cadence doubles as requested: compact every 2h (was 1h) and only past 60% / 40% context (documented as 30/20, previously unenforced). Auto-clear ships DISABLED — it discards context rather than summarising it, so a silent timer seemed the wrong default; the rule is built and one toggle from on. Webhooks go from one endpoint to many, multiplexed over a single server and tunnel, keyed by id in the path with a per-endpoint secret and a user-editable JSON Schema. Unknown ids and wrong secrets stay indistinguishable so the surface cannot enumerate which webhooks exist. Webhooks and org peers share a trust gate — strict / allow-all / communication-only. Held messages answer 202 awaiting-approval and land in Trigger History with approve/reject; this is the hive's first inbound approval queue. ORG_API_KEY is config and UI only, as scoped — no transport yet. Both surfaces (Settings and the Triggers tab) render off one store mirror, so an edit in either appears in the other without a refetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> | 2 个月前 | |
feat(triggers): schedule on weekdays at a time, not just on an interval A scheduled trigger could only say "every N milliseconds", which cannot express "weekday mornings". An interval drifts against the clock, and a 24h one started at 15:00 fires at 15:00 forever. A mission can now carry days plus a time, and when it does that replaces the interval. The picker is a WHEN field with two tabs. "every…" is the old IntervalPicker, untouched. "on days…" is seven toggles in Date.getDay() order, so nothing sits between what is clicked and what is stored, plus a native time field and weekdays / every day shortcuts. The row chip reads "weekdays at 09:00" instead of an interval, and a weekly mission can say when it runs NEXT before it has ever run, which the interval kind cannot. The arithmetic is a pure module with its own tests, because time is not uniform: - Local time, built from calendar fields, never from added milliseconds. The week the clocks change is 169 hours long, so "Monday 09:00" plus 7 * 86400000 lands at 08:00. There is a DST test that pins this in a fixed zone. - A slot missed while the machine slept fires on wake, within 6 hours. Asleep at 09:00 and opened at 10:30 should still run; closed Friday and opened Monday should not. This matches what the interval scheduler already does on boot. - Firing is strictly after "now", and the catch-up is gated on lastFiredAt, because the scheduler rebuilds every timer whenever ANY mission is saved. Without both, editing one mission would re-dispatch another. - The re-arm passes `now` as the last-fired floor after a fire, so a failed writeConfig costs a stamp rather than spinning on a zero delay forever. intervalMs is kept on a weekly mission so switching back restores the cadence. Switching to interval sends weekly: undefined explicitly — the backend merges by spread, so omitting the key would leave the old schedule in place. Heartbeat missions stay interval-only: the beat adapts to how busy the floor is, so pinning it to Tuesdays would be a lie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018R7wTroJDW1MbHC8Qi9eaY | 2 个月前 | |
fix(windows): spawn the direct-executable npm shim instead of cmd.exe v0.4.4's winfix decoded npm shims to "interpreter + script" and spawned that with an argv array, so the multi-line hive protocol survived. It never engaged for OpenCode. `opencode-ai`'s bin is `./bin/opencode.exe` — a compiled binary, not a JS script — so npm writes an interpreter-less shim: "%dp0%\..\opencode-ai\bin\opencode.exe" %* parseNpmCmdShim modelled only the two-token form. With one quoted token and nothing before it, `before` is empty, `word` is undefined, and it returned null — falling straight back to the cmd.exe path that truncates the prompt at its first newline. Every Windows OpenCode install hit this, deterministically. The agent booted, rendered, looked healthy, and had no idea it had an inbox. Diagnosed on macOS: `opencode-ai`'s package.json points bin at a Mach-O binary, and npm's own `cmd-shim` generates the shape above for a target with no shebang. The three new tests use that verbatim output rather than a hand-written guess. The resolver now returns `interpreter: null` for a direct-executable shim and the spawn path hands the binary its args with no script prepended — same program, no shell parser in between. Also: the cmd.exe fallback was SILENT, which is how this survived a release. It now warns with the target it could not decode and whether a multi-line argument is actually at risk in that spawn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> | 2 个月前 | |
fix: add Windows fallback discovery for Antigravity CLI (#592) - add shared Windows fallback candidate generation for command resolution - resolve Antigravity CLI from %LOCALAPPDATA%\agy\bin\agy.exe - normalize agy command variants without changing caller command semantics - fail closed for missing or invalid LOCALAPPDATA roots while supporting UNC paths - reuse fallback candidates in PTY resolution without changing where, PATHEXT, cache, shim, or spawn behavior - add Windows regression coverage for fallback resolution and excluded antigravity.cmd alias | 1 个月前 | |
review: leave the worker's own work order out of the report to god; one line per subject Self-review (CONTRIBUTING: run your agent over your PR). The order is dispatched in conversation worker-<reqId> and is exactly what the worker just completed; workers rarely file it, so nearly every release would have told god it was filed unread, a god turn per release for nothing. settleInbox now reports each filed message's conversation and the done path skips that one. Agent-written ids and subjects are folded onto one line so a newline cannot forge extra list entries. The rename-failure comment no longer promises a retry that does not exist: the message simply stays pending. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> | 1 个月前 | |
fix(worker): resolve default command from requested provider - derive omitted worker commands from explicit provider presets - preserve explicit command precedence and custom-provider fallbacks - document provider and command precedence in the generated hive protocol - add regressions for provider-only, custom, and invalid-provider launches | 1 个月前 | |
security: keep the OS sandbox on in auto mode Auto mode dropped every sandbox because a hive worker writes to its agent folder under <harnessHome>/hive/agents/<id>/, outside the project cwd. That is a path problem, not a reason to run unsandboxed. - codex: -a never -s workspace-write instead of the full bypass; hive.ts adds the agent dir, hive root and palace via --add-dir. An explicit posture on the command line (including the old bypass flag) still wins. - claude: the per-session settings.json now enables sandbox.enabled with filesystem.allowWrite plus permissions.additionalDirectories for the same dirs. bypassPermissions only silences prompts; verified live that the sandbox still denies writes outside cwd and the listed dirs. - failIfUnavailable stays off so platforms without a sandbox spawn as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5X7vQmQDwrTCn2qBzuBiJ | 2 个月前 | |
review: read telemetry once per worker per beat; re-home decide()'s docblock; doc wording Self-review (CONTRIBUTING: run your agent over your PR). getAgentUsage() was called twice per worker per beat, and with no live OTel each call runs the transcript fallback (a registry read plus a scan of the transcript directory): the beat now reads it once and derives both facts. decide()'s JSDoc had been orphaned above explain(); it is back above decide(), with the bounded-memory rationale that had been dropped. lastHoldReportAt sits with the other fields. lastActivityAt's doc and the test header describe the evidence as it is now: a token-bearing sample or a span, plus turn-proving hook events. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> | 1 个月前 | |
fix(wake): announce only newly arrived inbox mail | 1 个月前 | |
fix: make dependencies available in isolated worktrees An isolated worktree does not include the base checkout's dependencies, so project validation can fail before it checks an agent's change. Link the dependencies after creating the worktree, while preserving existing entries and allowing the spawn to continue if linking fails. Before deciding whether a worker worktree is dirty, remove only a dependency link that resolves to the base checkout's node_modules. This keeps unignored links from preventing automatic cleanup while preserving real directories and foreign links. Use Windows junctions so directory links work without elevated privileges. The regression coverage verifies link creation, skip and failure paths, cleanup without a .gitignore, Windows link selection, and that git worktree removal leaves the base dependencies intact. | 2 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 25 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 13 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 19 天前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 3 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 4 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 3 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 2 个月前 |