Commit graph

4 commits

Author SHA1 Message Date
Marc Chan
e14b8092ea
feat: add Orbit activity summaries (#681)
* feat: add Orbit activity summaries

* fix(orbit): make runs navigable while agent continues

* fix(web): widen minimum chat panel

* feat: support Orbit template selection

* fix(daemon): avoid bogus skill side-file preflight

* fix(web): collapse orbit artifact project cards

* fix(web): preserve orbit project card titles

* fix: improve Orbit run daily briefing

* fix: handle Orbit digest data failures

* fix: load Orbit templates and connector tools reliably

* fix: keep Orbit summary counts consistent

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: apply Orbit template skill context

* fix: cache and curate connector tools for Orbit

* fix: align Orbit defaults and connector discovery

* fix: simplify Orbit template settings

* fix: move connectors into settings

* fix: compact connector settings catalog

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: address Orbit PR feedback

Generated-By: looper 0.6.1 (runner=fixer, agent=opencode)

* fix: prevent connector action button from stretching into pill

The icon-only connect/disconnect buttons in the embedded connectors
catalog inherited min-width: 92px / 106px from the non-embedded pill
rules, overriding the 24px square sizing and causing the buttons to
overlap the card head text. Reset min-width to 0 in the embedded
icon-only rule so the compact square layout holds.

* fix(web): align live artifact file rows

* fix: clean up Orbit connector settings lifecycle

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix: address Orbit review regressions

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* feat(web): localize Orbit and connector settings

* feat(web): gate Orbit runs without connectors

* feat(web): refine connector settings UX

* feat(web): safeguard Composio key clearing

* fix(web): refresh Composio tool badges

* feat(web): show connector logos

* feat(daemon): localize Orbit prompt window

* fix(daemon): clarify blocked connector callback closes

* test(daemon): harden flaky async probes

* fix(web): align Indonesian connector locale keys

* test(web): align connector browser props

* fix(web): preserve explicit credential clears

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): time out Composio logo proxy fetches

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): localize Indonesian connector settings copy

Translate the new connector settings strings in the Indonesian locale and lock them with a regression test so this surface no longer silently falls back to English.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): preserve discovered connector tools

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): preserve onboarding autosave completion

Keep settings autosave from clearing onboarding completion after the close gesture, and expose the desktop main types from source so workspace validation can typecheck packaged imports without a prior desktop build.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): defer Composio catalog cache hydration

Load persisted Composio catalog data only after the runtime data directory is configured so startup cannot read another namespace's cache. Add a regression test that exercises the module-load singleton path.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): treat discovery completion independently

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): preserve latest settings draft on close

Use the latest persisted settings draft when the dialog closes so onboarding completion does not race a stale daemon sync and overwrite newer Orbit/template selections.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): avoid syncing draft Composio key on Orbit run

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): localize Orbit settings copy

Translate the new Indonesian Orbit and autosave strings so the settings UI no longer falls back to English and the locale regression stays covered.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): prefer fresh connector catalog state

Keep refetched connector status/auth data authoritative while retaining discovery-only tool metadata so the connectors UI stays consistent after refreshes.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): declare Indonesian locale fallback keys explicitly

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): inline Indonesian fallback strings for CI

Replace the Indonesian locale's per-key English lookups with explicit strings so workspace typecheck no longer depends on brittle build-mode resolution in CI.

Add a regression test that blocks those per-key English lookups from reappearing in the CI-sensitive fallback sections.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): restrict proxied connector logos to image MIME types

Reject non-image upstream logo responses so the daemon never serves third-party HTML from its localhost origin.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* test(e2e): align settings dialog regressions

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): decouple Orbit runs from media sync failures

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): keep SPA catch-all export-compatible

Disable dynamic catch-all params for the exported SPA shell so Next.js static builds can emit the root route again. Add a regression test covering the route config against the web export mode.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): preserve Orbit config and workspace routes

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): block SVG in connector logo proxy

Reject SVG and other unsafe proxied logo responses so third-party logo content cannot execute under the daemon origin, while keeping raster logo fetches working and making rejected responses non-cacheable.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): fall back to static catalog for empty cache

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): disable Orbit run before connector gate resolves

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(desktop): export shipped desktop types

Point the desktop ./main type export at the generated declaration so installed consumers resolve the published file set.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): restore persisted question form selections

Render historical submitted answers directly so reloaded question forms keep their locked selections visible.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): retry forced media sync autosave

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): keep Composio logo timeout through body read

Keep the Composio logo fetch timeout active until the response body is fully consumed so stalled body reads abort and clear the inflight cache entry. Add a regression test that proves a delayed body read times out and the next request can recover.\n\nGenerated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): refresh Orbit gate after connector auth

Re-check connector availability when the settings window regains focus so Orbit unlocks as soon as a connector finishes authenticating in the same settings session.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): keep connector detail tool lists intact

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): ignore malformed Orbit summaries

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(e2e): stabilize design-system multi-select flow

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): cap Composio logo cache growth

Bound the Composio logo cache with LRU eviction and expired-entry pruning so repeated untrusted logo requests cannot grow daemon memory without limit.

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(daemon): bound proxied Composio logo payloads

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): align autosave settings tests

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): remove stray CSS conflict marker

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fixer: address PR #681 follow-up items

Generated-By: looper 0.6.2 (runner=fixer, agent=opencode)

* fix(web): restore restart routes and connector flows

* fix(web): keep SPA export route static

* fix(web): stabilize chat scroll tests

---------

Co-authored-by: lefarcen <935902669@qq.com>
2026-05-08 14:27:46 +08:00
Tom Huang
56bf6ee1b6
feat: agent-callable research command and /search (#615)
* feat: pre-generation research (Tavily) for grounded generation

Adds an optional pre-generation research step so the agent can produce
slides / prototypes / decks grounded in real sources instead of guessing.

User flow:
  1. Settings -> Tavily Search -> paste API key (or set TAVILY_API_KEY).
  2. Click the new Research button in the chat composer.
  3. On send, the daemon runs a Tavily search, prepends the findings
     as a <research_context> block ahead of the system prompt, and
     spawns the agent. Research progress shows up as status pills in
     the chat stream; the agent cites sources inline as [1]/[2]/...

Phase 1 surface:
  - Single provider (Tavily), single depth ('shallow'), no LLM
    synthesis pass (Tavily's `answer` is the summary).
  - Composer toggle only; no popover / depth picker yet.
  - Reuses the existing `status` SSE agent payload + StatusPill UI
    so no new event variants or renderer code are needed.

Layers touched:
  - contracts: ResearchOptions / Source / Findings DTOs;
    ChatRequest.research; export from index.
  - daemon: apps/daemon/src/research/{index,tavily}.ts orchestrator
    + provider; tavily added to MEDIA_PROVIDERS and ENV_KEYS; hook
    in startChatRun before prompt assembly.
  - web: ChatComposer toggle + ChatSendMeta; threaded through
    ChatPane / ProjectView / streamViaDaemon into ChatRequest.

Side fix (required to land the feature, but useful on its own):
  contracts internal relative imports lacked the `.js` suffix that
  NodeNext module resolution requires. This was already breaking
  `pnpm --filter @open-design/daemon typecheck` on main; without the
  fix, none of the new research types were visible to the daemon.
  All internal contracts imports now carry `.js`.

Spec: specs/current/research-feature.md (phases 2-4 outlined for
follow-up: composer popover, multi-provider, deep recursion, example
skills with research_recommends).

Verified:
  - pnpm --filter @open-design/contracts typecheck/test
  - pnpm --filter @open-design/daemon typecheck (the chokidar
    project-watchers test is a pre-existing flake, unrelated)
  - pnpm --filter @open-design/web typecheck
  - node scripts/verify-media-models.mjs

* fix(daemon): clamp Tavily max_results to 20

Tavily's /search endpoint requires `max_results` in [0, 20]; sending a
larger value (e.g. when `research.depth: "deep"` resolves to 30) returns
400 and `runResearch` silently falls back to no-research. Clamp at the
provider boundary so Phase 2 depth tiers above 20 still produce results
instead of failing the request.

Generated-By: looper 0.6.1 (runner=fixer, agent=claude-code)

* Remove stale research merge leftovers

* Add agent-callable research search

* Fix Indonesian locale typecheck

* Fix research command invocation edge cases

* Harden slash search prompt expansion

* Honor research source caps in command contract

* Require search reports in design files

* Add research data provider settings

* Wire web research provider fallback order

* Update research provider fallback wording

* Revert "Update research provider fallback wording"

This reverts commit 86fb6001e3.

* Revert "Wire web research provider fallback order"

This reverts commit 4c9e16036b.

* Revert "Add research data provider settings"

This reverts commit 23630d1746.

* Add Dexter and Last30Days research skills

* Add DCF and Last30Days OD skills

* Add Last30Days and Dexter skills

* Resolve research review threads

---------

Co-authored-by: a1chzt <chizblank@gmail.com>
2026-05-08 10:33:44 +08:00
Marc Chan
c3d9136a0c
Add live artifacts and Composio connector catalog (#381)
* docs: add live artifacts implementation spec

* docs: align live artifacts implementation plan

* Ralph iteration 1: work in progress

* Ralph iteration 2: work in progress

* Ralph iteration 3: work in progress

* Ralph iteration 4: work in progress

* Ralph iteration 5: work in progress

* Ralph iteration 6: work in progress

* Ralph iteration 7: work in progress

* Ralph iteration 8: work in progress

* Ralph iteration 9: work in progress

* Ralph iteration 10: work in progress

* Ralph iteration 11: work in progress

* Ralph iteration 12: work in progress

* Ralph iteration 13: work in progress

* Ralph iteration 14: work in progress

* Ralph iteration 15: work in progress

* Ralph iteration 16: work in progress

* Ralph iteration 17: work in progress

* Ralph iteration 18: work in progress

* Ralph iteration 19: work in progress

* Ralph iteration 20: work in progress

* Ralph iteration 21: work in progress

* Ralph iteration 22: work in progress

* Ralph iteration 23: work in progress

* Ralph iteration 24: work in progress

* Ralph iteration 25: work in progress

* Ralph iteration 26: work in progress

* Ralph iteration 27: work in progress

* Ralph iteration 28: work in progress

* Ralph iteration 29: work in progress

* Ralph iteration 30: work in progress

* Ralph iteration 31: work in progress

* Ralph iteration 32: work in progress

* Ralph iteration 33: work in progress

* Ralph iteration 34: work in progress

* Ralph iteration 35: work in progress

* Ralph iteration 36: work in progress

* Ralph iteration 37: work in progress

* Ralph iteration 38: work in progress

* Ralph iteration 39: work in progress

* Ralph iteration 40: work in progress

* Ralph iteration 41: work in progress

* Ralph iteration 42: work in progress

* Ralph iteration 43: work in progress

* Ralph iteration 44: work in progress

* Ralph iteration 45: work in progress

* Ralph iteration 46: work in progress

* Ralph iteration 47: work in progress

* Ralph iteration 48: work in progress

* Ralph iteration 49: work in progress

* Ralph iteration 50: work in progress

* Ralph iteration 51: work in progress

* Ralph iteration 52: work in progress

* Ralph iteration 53: work in progress

* Ralph iteration 54: work in progress

* Ralph iteration 55: work in progress

* Ralph iteration 56: work in progress

* Ralph iteration 57: work in progress

* Ralph iteration 58: work in progress

* Ralph iteration 59: work in progress

* Ralph iteration 60: work in progress

* Ralph iteration 61: work in progress

* Ralph iteration 62: work in progress

* Ralph iteration 63: work in progress

* Ralph iteration 64: work in progress

* Ralph iteration 65: work in progress

* Ralph iteration 1: work in progress

* Ralph iteration 2: work in progress

* Ralph iteration 3: work in progress

* Ralph iteration 4: work in progress

* Ralph iteration 5: work in progress

* Ralph iteration 6: work in progress

* Ralph iteration 8: work in progress

* Ralph iteration 9: work in progress

* Ralph iteration 17: work in progress

* Add Composio-backed connectors

* Add Composio-backed connector catalog

* Fix connector callback flow

* Update live artifact connector refresh

* Fix live artifact refresh updates

* Improve live artifact viewer toolbar

* Refine live artifact source tabs

* Expand Composio connector catalog

* Improve Composio connector browsing

* Fix artifact refresh source safety checks

Generated-By: looper 0.4.1 (runner=fixer, agent=opencode)

* Fix live artifacts PR feedback

Generated-By: looper 0.5.0 (runner=fixer, agent=opencode)

* Fix live artifact preview CORS validation

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix connector OAuth IPv6 loopback hosts

Allow bracketed IPv6 loopback Host headers when deriving connector OAuth callback URLs so IPv6-bound daemons can complete connection flow.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Preserve live artifact refresh permissions

Respect explicit refresh permission choices during live artifact create and update flows so revoked connector sources remain gated.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix live artifact preview cache freshness

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix live artifact refresh validation

Guard manual refreshes with local daemon checks and reject daemon_tool sources without a toolName before refresh execution.

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix Composio credential invalidation

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix live artifact CORS methods

Generated-By: looper 0.0.0-dev (runner=fixer, agent=opencode)

* Fix workspace validation

Restore media config test isolation under Vitest setup data-dir overrides and add the missing French live artifact display copy so the workspace test suite stays aligned.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector safety filtering

Keep agent-preview connector listings aligned with execution safety policy and prune stale Composio OAuth state records before they accumulate.

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix agent runtime cleanup

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix live artifact daemon access

Validate local-only live artifact routes against the peer socket address and pass daemon-resolved CLI paths to ACP MCP descriptors.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector run limit pruning

Evict stale connector rate-limit buckets so long-lived daemon processes do not retain per-run entries indefinitely.\n\nGenerated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Fix connector compact schemas

Generated-By: looper 0.5.2 (runner=fixer, agent=opencode)

* Improve connector connection feedback

* Adjust connector gate positioning

* Fix live artifact refresh commits

Avoid marking refresh candidates failed after snapshot or state persistence errors by deferring live artifact mutations until the durable refresh metadata is written. Also align connector OAuth callback host validation with daemon loopback handling.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Improve connector search relevance

* fix(daemon): harden connector connection state

Require loopback daemon validation before connector connect side effects and only clear provider-owned connector statuses during credential reset.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): guard connector disconnect route

Require local daemon request validation before connector disconnect side effects.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): guard composio config updates

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): dispatch live artifacts mcp first

Route the live-artifacts MCP server before the generic MCP CLI so od mcp live-artifacts starts the dedicated server instead of failing generic argument parsing.\n\nGenerated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix(daemon): handle integer connector schemas

Allow JSON Schema integer connector inputs while preserving fractional-value validation so generated connector tool schemas accept valid page sizes and limits.

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* fix: align live artifact refresh error codes

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Fix live artifact connector refresh flow

* Update live artifact design cards

* Add beta badge to live artifact form

* Remove live artifact tile model

* Fix live artifact refresh sync

* Fix live artifact MCP refresh durability

Generated-By: looper 0.5.4 (runner=fixer, agent=opencode)

* Fix live artifact refresh safety

Enforce persisted refresh opt-out and connector auto-read gating before refresh sources execute.

Generated-By: looper 0.5.5 (runner=fixer, agent=opencode)
2026-05-05 16:42:11 +08:00
Xinmin Zeng
76b41ffef4
fix(daemon): expose skill resources via cwd-relative aliases (#435)
* fix(daemon): expose skill resources via cwd-relative aliases (#430)

Skill bodies referenced repository-absolute paths in their preamble and
relied on every agent CLI honouring `--add-dir` (or its equivalent) to
open them. Most agents (codex, gemini, opencode, cursor-agent, qwen,
pi, hermes, kimi) have no such flag, and Claude Code's directory-access
policy can still block these reads under specific permission/trust
configurations even when `--add-dir` is set, so SKILL side files fail
to load mid-turn.

Stage `.od-skills` and `.od-design-systems` directory links inside the
project cwd before spawning the agent, and rewrite the skill preamble
to address resources via the cwd-relative alias path. The alias
resolves through a directory junction (Windows) or symbolic link
(POSIX), so resources are reachable from inside every agent's working
directory without needing an extra-allowed-directories flag.

`extraAllowedDirs` is kept as a belt-and-suspenders fallback for
Claude/Copilot — if the alias cannot be created (e.g. the user has a
pre-existing real entry under one of the reserved names), `--add-dir`
still covers the absolute paths.

`ensureCwdAliases` is idempotent, repoints when the target moves, and
refuses to overwrite a non-symlink entry under the reserved name to
avoid clobbering user data.

Closes #430.

* fix(daemon): address review feedback — copy active skill, dual preamble paths

Round 2 of PR #435 addressing the unresolved P1/P2/P3 review threads
from @chatgpt-codex-connector, @lefarcen and @mrcfps.

- P1 writable-symlink. Replace the directory link with a per-project
  `fs.cp` of the active skill into `<cwd>/.od-skills/<folder>/`.
  Writes through the staged copy no longer mutate the shipped repo
  resource. A regression test pins this down so a future "optimisation"
  that re-introduces a symlink would fail loud.
- P1 absolute fallback. Rewrite the skill preamble to advertise both
  the cwd-relative alias path (primary) and the absolute path
  (fallback). When `stageActiveSkill` is skipped (no project cwd) or
  errors, Claude/Copilot still reach the resource via the absolute
  path covered by `--add-dir`; other agents that fall back to
  `PROJECT_ROOT` as cwd reach it natively because the absolute path
  is in-cwd there.
- P2 symlinked source root. Use `stat()` instead of `lstat()` on the
  source so a symlinked `SKILLS_DIR` or skill folder is followed
  rather than skipped.
- P2 stale ordering. `ensureCwdAliases` is gone; staging now happens
  independently of prompt composition and never silently diverges
  from what the preamble advertises.
- P3 unsafe spec name. Validate `folderName` against an explicit
  segment policy (no path separators, no dot segments, no absolute,
  no null bytes) so a malformed caller cannot escape the alias root.
- P3 strong types. Drop the `@ts-nocheck` pragma on `cwd-aliases.ts`
  and type the public API (`SkillStagingResult`, `SkillStagingLogger`).

Also drops the `.od-design-systems` alias entirely. Design-system
bodies are read by the daemon and folded into the system prompt
directly, so an agent never has to open them through the filesystem;
exposing that tree as cwd-visible was unnecessary surface area.

Test additions: 17 cases in `cwd-aliases.test.ts` covering the copy
semantics, the write-barrier regression, symlinked-source handling,
legacy-symlink upgrade, alias-root collision refusal, cwd=null guard,
and a parameterised set of unsafe folder names.

* fix(daemon): unify effectiveCwd and use junctions on Windows test fixtures

Round 3 of PR #435 addressing two non-blocking follow-ups from
@mrcfps:

- server.ts: collapse the chat-handler `cwd` resolution into a single
  `effectiveCwd = cwd ?? PROJECT_ROOT` and feed it consistently to
  `buildArgs`, `spawn`, and the ACP/Pi RPC session adapters. Previously
  `spawn(... { cwd: cwd || undefined })` let no-project runs inherit
  whatever working directory the daemon process was started from
  (packaged daemons / service launches do not start in the workspace
  root), which made the "absolute path is in-cwd" guarantee in the new
  skill preamble untrue for that exact case the comment claimed to
  cover. With `effectiveCwd`, agents in no-project mode land in
  `PROJECT_ROOT` and the absolute fallback path emitted by the preamble
  is genuinely a relative-to-cwd path.

- cwd-aliases.test.ts: replace `symlinkSync(..., 'dir')` fixtures with
  a platform-aware `dirLinkType` (`'junction'` on Windows, `'dir'`
  elsewhere). Directory symlinks on Windows require
  SeCreateSymbolicLinkPrivilege / Developer Mode, which is not enabled
  on most CI images, so the previous form would fail the daemon test
  suite on Windows runners even though the production code (which
  uses `fs.cp`) does not need symlinks at all.
2026-05-04 17:48:20 +08:00