Commit Graph
40 Commits
Author SHA1 Message Date
esengine 55c307834e fix(encoding): a file is GB18030 only if a write can restore its bytes
edit_file on a UTF-8 file carrying CJK text rewrote bytes it never touched:
continuation bytes of characters far from the edit came back as 84 31 A4 37.

Detect took a nil error from the GB18030 decoder as proof the file was
GB18030. The decoder never fails: it maps every invalid sequence to U+FFFD.
So any file that was not valid UTF-8 -- one stray byte is enough -- was read
as GB18030, and the read-modify-write tools (edit_file, multi_edit,
delete_range, write_file over an existing file), checkpoint restore and the
config migrations then re-encoded every unreadable sequence as the GB18030
form of U+FFFD.

Detect now names a charset only when decoding and re-encoding restores the
input byte for byte. GB18030 is tried first, then GBK, which restores what
GB18030 cannot such as CP936's single-byte euro 0x80, so such a file stays
editable in its own encoding; anything else is LossyUTF8, whose bytes pass
through untouched. The new Kind is appended because checkpoints persist it
by value.

GBK cannot hold every character, so Encode now returns an error instead of
falling back to UTF-8, which rewrote a whole CP936 file as UTF-8 when an edit
added a character such as an emoji. A legacy charset that cannot represent a
character answers an *UnencodableError naming it, its byte offset and the
charset, with ErrUnencodable as its identity; the file tools, checkpoint
restore and the config migrations refuse the write and leave the file as it
was. Fixtures encode through MustEncode.

Readers that hold only part of a stream say so rather than inherit the
whole-file answer:
- DetectFragment runs the round trip on the prefix the decoder consumes when
  not at EOF, so a character the cut split is dropped structurally. grep's
  8 KiB peek, read_file's 256 KiB sample and the desktop's 2 MiB file preview
  use it; the preview's next page starts at the split character, and lossy
  text is shown as text rather than as binary.
- DecodeOutput is the one entry point for bytes that are only displayed.
  The producer says which ends its bound cut (Cut{Head, Tail}); UTF-8 is read
  without a character split at a cut end, and an uncut end keeps its bytes,
  since a GBK pair there is output rather than half a rune. What no charset
  restores is still read as GB18030. Hook output passes its tail cut when
  truncated.
- DetectAndDecode decodes a legacy charset once; the edit tools and
  DecodeToUTF8 use it instead of detecting and decoding separately.

Tests that fail before this change (on the base or an earlier revision of it)
and pass here:
- TestE2EEditKeepsBytesOfUTF8FileWithStrayByte: e4 b8 ad became
  e4 b8 84 31 a4 37 outside the edit (fails on origin/main-v2).
- TestDetectUTF8WithStrayByteIsNotGB18030 (fails on origin/main-v2).
- TestWritesRefuseCharacterCP936CannotHold (edit_file, multi_edit,
  write_file): nil error and the whole CP936 file rewritten as UTF-8.
- TestDecodeOutputTrimsCutUTF8Edges: UTF-8 cut at the front read as mojibake.
- TestDecodeOutputKeepsUncutCodePageEdges, TestDecodeHookOutputKeepsUncutCodePageEdges:
  GBK pairs at an uncut edge (bc db, b0 a1 0d 0a, 6f 6b 20 e4 a1,
  b0 a1 20 6f 6b) were dropped as partial UTF-8 runes.
- TestEditKeepsCP936FileWithEuroByte: an edit adding Chinese wrote UTF-8
  into a CP936 file.
- TestGrepGB18030PeekWithoutNewline, TestGrepGB18030PeekCutMidCharacter:
  no matches past a peek cut mid-character.
- TestReadFileGB18030SampleWithoutNewline: garbled text past the sample.
- TestReadFileGB18030TruncatedWithoutNewline,
  TestReadFileGB18030TruncatedMidCharacter (desktop): the 2 MiB preview of a
  GB18030 file was garbled or ended in U+FFFD.
- TestDecodeHookOutputReadsTruncatedCodePageText,
  TestDecodeHookOutputReadsCP936Euro: mojibake for cut or euro-carrying cmd.exe output.
- TestEncodeRefusesCharacterTheCharsetCannotHold, TestDetectGBKWithEuroByte,
  TestDetectFragmentDropsTheSplitCharacter, TestDecodeOutputReadsCutCodePageText:
  the API's contract.
2026-09-26 11:52:22 -07:00
SivanCola 0556d1b528 fix(sessioncatalog): harden disposable SQLite lifecycle and non-blocking tree
Problem
Desktop session discovery still risked quarantining healthy catalogs on
busy/permission errors, rebuilding by renaming the live DB first, dropping
repair work beyond a fixed queue, and serving stale ProjectTree search results.
Off-lock controller builds could also re-publish removed MCP tools onto a shared
host, and shell-only project trees never expanded enough for lazy topic pages.

Root cause
Open treated every open failure as corruption; Rebuild swapped before validation;
repair enqueue was lossy; ProjectTree dropped concurrent queries while loading;
shared Host mutation raced plugin config changes; and active-folder expansion
required already-loaded topic children.

Fix
- Introduce internal/projectiondb for open/migrate/rebuild/quarantine with
  corruption-only quarantine, future-schema memory fallback, atomic rebuild,
  and 0700/0600 permissions.
- Route sessioncatalog through projectiondb; empty CacheDir uses memory; resume
  scan_cursor; drain unknown repairs from the durable index; bound per-topic
  session pages.
- ProjectTree uses request sequence + debounce and expands shell folders by
  workspace identity so lazy pages and bench mock load.
- Capture extensionGeneration around off-lock boot.Build and abandon publication
  when plugin/MCP config changes mid-build.
- Fix lint modernize issues and Windows migration-marker test flake.

Verification
- go test ./internal/sessioncatalog ./internal/projectiondb ./internal/pluginpkg
- go test -race ./internal/sessioncatalog ./internal/projectiondb
- cd desktop && go test -run 'Catalog|TopicMigration|ProjectTree.*Migrat'
- pnpm --dir desktop/frontend run typecheck
2026-08-11 01:38:27 +08:00
SivanCola f882c708b5 refactor: satisfy session catalog ownership budgets
Problem:
The initial session catalog implementation exceeded incremental repository size and complexity budgets in several existing owner files, and Desktop CI reported stale helpers plus two modernization findings.

Root cause:
Catalog lifecycle, runtime projection, frontend topic presentation, mock bindings, plugin compatibility, and CLI wiring were colocated with already-large modules. Removing the eager project tree also left legacy runtime-tree helpers unused.

Fix:
- split catalog lifecycle from runtime projection and metadata mapping
- extract frontend catalog contracts, mock bridge, topic presentation, and count formatting
- isolate plugin compatibility, session projection, and CLI completion wiring
- remove obsolete eager-tree helpers and address CI modernization findings
- preserve behavior without changing repolint baselines

Verification:
- go run ./tools/repolint
- golangci-lint v2.12.2: 0 issues
- go test affected root packages
- focused Desktop catalog and workspace regression tests
- pnpm test:all
- pnpm build
2026-08-10 17:41:41 +08:00
SivanCola bf19574d3a feat(desktop): add resilient session catalog
Problem:
Desktop startup and project-tree queries could synchronously migrate and decode legacy session JSONL, blocking navigation, creation, and shutdown.

Root cause:
Session discovery, metadata repair, controller admission, and plugin startup shared UI-critical paths and broad lifecycle locks.

Fix:
- add a disposable SQLite session catalog with incremental reconciliation, repair checkpoints, corruption recovery, and memory fallback
- expose revisioned cursor pagination and migrate the desktop project tree to lazy loading
- move controller construction outside lifecycle admission and isolate incompatible plugin startup
- add session doctor and reindex commands while preserving authoritative JSONL and metadata
- retain CGO-free builds with platform-specific fallbacks

Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/sessioncatalog ./internal/agent
- cd desktop && go test -race .
- go vet ./... in root and desktop
- cd desktop/frontend && pnpm test:all && pnpm build
- CGO_ENABLED=0 cross-builds for supported macOS, Windows, and Linux targets
2026-08-10 17:16:26 +08:00
SivanCola 8cc7279c86 refactor(plugin): keep compatibility changes within lint budgets
Move Claude root-instruction helpers out of the oversized parser file and keep marketplace fixtures line-neutral while preserving the official skill-based coverage.

Verification: go run ./tools/repolint; go test ./...; ./scripts/cache-guard.sh.
2026-08-10 02:03:33 +08:00
SivanCola 1c6776d9d9 fix(plugin): align Claude root instructions with upstream
Problem: Reasonix synthesized a SessionStart context hook from a Claude plugin root CLAUDE.md, although Claude Code does not load that file as plugin project context.

Root cause: Claude manifests shared the Codex compatibility helper that always appended root instructions.

Fix: keep supported Claude convention imports without the synthetic root hook, document the distinction, and update install-source fixtures to use official skills.

Verification: go test ./...; ./scripts/cache-guard.sh; focused plugin and install-source tests; desktop plugin/theme tests.
2026-08-10 01:59:34 +08:00
SivanCola bd97fecc5b fix(plugin): make native v2 resources explicit
Problem: Native v2 manifests could silently import Claude compatibility sidecars, including repository instructions and executable hooks.

Root cause: parseNativeV2 applied Claude compatibility discovery after validating the explicit manifest.

Fix: Treat the native v2 manifest as the complete capability boundary while preserving compatibility discovery for Codex and Claude manifests. Document the contract and cover it with a regression test.

Verification: go test ./...; cd desktop && go test . -run "Plugin|Theme" -count=1.
2026-08-10 00:08:02 +08:00
SivanCola 500d21a283 Fix runtime ownership and v2 manifest boundaries
Problem:
Runtime publish gates, receipts, file priors, message deduplication, and stream cancellation were process-global, so rebuilding one session could drain or overwrite another session's lifecycle evidence. The v2 manifest path also still exposed migration behavior for versioned manifests.

Root cause:
Lifecycle state was wired through package globals instead of the controller/session lineage, and migration/parser compatibility retained assumptions from the unpublished manifest v1 design. Drain cancellation also allowed callbacks to register after a generation had already expired.

Fix:
Introduce an explicit RuntimeOwner carried through boot, controller, provider, UI, host streams, receipts, and file-write compensation. Reuse it only within a rebuild lineage, defer replacement publication until commit, reject draining turns, make late drain registrations cancel immediately, and enforce v2-only versioned manifests while limiting explicit migration to pre-extension manifests without apiVersion.

Verification:
- go test -race ./internal/agent ./internal/boot ./internal/control ./internal/extension ./internal/extension/providerext ./internal/extension/uihub ./internal/tool/builtin -count=1
- go test ./internal/extension ./internal/pluginpkg ./internal/recovery ./internal/agent ./internal/control ./internal/boot -count=1
- go test ./... -count=1 (sdk/go)
- focused Desktop plugin lifecycle tests
2026-08-08 02:29:40 +08:00
SivanCola 3552822776 feat: add generation-safe runtime composition 2026-08-08 01:03:45 +08:00
YHH 4fbbca6a99 refactor(go): adopt modern stdlib idioms and gate them (#7831)
* refactor(go): adopt modern stdlib idioms across kernel and desktop

Mechanical sweep with golangci-lint's modernize/intrange/copyloopvar/
usestdlibvars fixers plus hand-fixes for what they could not rewrite:
slices.Contains, min/max, slices.Backward, WaitGroup.Go, reflect.TypeFor,
maps.Copy, strings.Cut/CutPrefix/SplitSeq, strings.Builder, range-over-int.
Net -549 lines with no behavior change.

Three loops in session.go stay index-based: slices.Backward yields element
copies, so the fixer's rewrite silently dropped writes to DecisionReceipts
and ToolCalls (caught by TestDelivery* and TestDependentSameBatch*). All 49
slices.Backward sites were audited for that shape; only these three write
through the index, and they carry a nolint with the reason.

CaptureAfter's scan loop had an empty body and never ran any logic, so the
fixer could not express it. Drop it and the accessor it was the only caller
of. omitzero is deliberately untouched: turning omitempty into omitzero
changes the JSON written for nested structs, which is a wire-format change
to session files and the extension protocol.

* build(lint): gate the modern-idiom linters

Nothing stopped the codebase from drifting back to pre-1.22 idioms. Both
modules are now clean for modernize, intrange, copyloopvar, and
usestdlibvars, so enable them rather than leaving the sweep as a one-off.

omitzero stays disabled with the reason inline; usetesting, perfsprint, and
errorlint still have residue and land separately.
2026-08-07 15:30:33 +08:00
YHH e1079f2b58 fix(errors): compare errors through the unwrap chain, and gate it (#7834)
Every `err == Sentinel` and `err.(*T)` in the tree works today only because the
error happens to arrive unwrapped. That is not a property anyone maintains: one
`%w` added between producer and check silently stops the branch from firing, and
this same change adds 54 of them. So the comparisons are converted first, then
the wrapping.

Three helpers turned out to be hand-rolled reimplementations of the standard
library and are deleted rather than patched: remote.isPassphraseMissing,
remote.asKeyError (which open-coded the Unwrap() walk), and forward.containsErr
(which open-coded both the single- and multi-error walks). Call sites use
errors.As/errors.Is.

Two assertions deliberately keep `!=`: explainError and AnnotateToolSchemaError
must return the *same* error object, and errors.Is would still pass if they
started wrapping it. Both carry a nolint naming that.

errorlint is enabled in .golangci.yml; both modules are clean under it.
2026-08-07 15:05:46 +08:00
SivanCola f2bc54fab3 fix(release): restore cross-platform v1.20 gates
Summary:
- make extension content eviction deterministic and plugin path checks portable
- stabilize the parallel-task cancellation coverage test
- keep POSIX mode assertions meaningful on Windows and serialize tmux fixtures safely

Verification:
- go test -covermode=atomic ./...
- go test -race on all changed packages
- go vet ./...
- Windows test binaries cross-compiled for all changed packages
2026-08-05 21:41:51 +08:00
SivanCola 71ac1a4c9d feat(extension): streaming provider adapter for sidecar extensions
Stage 7 of the unified extension kernel / Extension Protocol v1 plan.

Problem: extension sidecars could declare providers at handshake, but the
host had no way to resolve or stream from them.

Fix:
- internal/extension/providerext: a provider.Resolver that merges each
  sidecar's catalog into the host resolver under
  plugin/<plugin>/<provider>/<model> refs, plus a provider.Provider whose
  Stream drives extension/provider/stream/open|cancel with broker-grade
  client semantics: 1-based contiguous sequence buffering (duplicates and
  stale chunks dropped, 256-slot bounded delivery queue, drain goroutine),
  stream/end.lastSeq as the frozen terminal boundary with a 1s gap timer
  that fails interrupted naming the missing sequence, context cancel that
  sends stream/cancel, and crash handling that drains buffered chunks then
  fails the stream interrupted — never falling back to another provider.
  Reasoning-policy interfaces (tool-call reasoning, reasoning round-trip,
  missing-reasoning warning) are answered from the descriptor, and effort
  is baked into the provider at resolve time like built-in providers.
- Ref conflicts are explicit: a sidecar descriptor colliding with an
  existing host ref fails BuildRuntime unless the plugin claimed
  provider:<ref> in runtime.replaces, in which case it replaces the base
  catalog entry (ParseSlot now accepts plugin-namespaced provider refs;
  the kernel's single-owner slot rules apply).
- internal/extension/providerconv: shared host-side provider<->protocol
  DTO conversions, extracted verbatim from the agent interceptor wiring
  so both consumers share one conversion path (agent behavior unchanged).
- sidecar.Client gains typed provider RPCs and a SetStreamRouter seam
  with a Disconnected signal used for crash propagation; boot wraps the
  effective resolver (local or remote broker — precedence preserved) and
  exposes it as BuildResult.ProviderResolver. Credentials never cross the
  wire: stream/open carries only ref, model, effort, and the request.

Verification: go test ./internal/extension/... ./internal/agent
./internal/boot pass (providerext and sidecar also under -race); boot E2E
with a re-exec fake sidecar resolves plugin/providerdemo/fake/x and
streams text+usage over the real wire; out-of-order/duplicate/missing/
late chunks, cancel, crash, catalog conflicts (unclaimed fails, claimed
wins), and concurrent-stream coverage included; protocol schema untouched
(generator -check green); golden baselines unregenerated; no new
dependencies.
2026-08-03 23:59:05 +08:00
SivanCola 0e90e2d1cf feat(extension): Extension Protocol v1 wire schema, generator, and sidecar lifecycle
Stage 5 of the unified extension kernel / Extension Protocol v1 plan.

Problem: Reasonix had no process model for code-carrying extensions — no
stable sidecar protocol, no schema generator to keep it honest, and no
lifecycle for spawning, supervising, and retiring extension processes.

Fix:
- internal/extension/protocol: a new public JSON-RPC 2.0 protocol
  (reasonix.extension.v1) with a frozen 16-method registry (initialize,
  initialized, shutdown, intercept, event, resources/changed,
  provider/catalog, provider/stream open|cancel|chunk|end, ui/action,
  ui/submit, host/ui/publish, host/ui/request, host/content/read), strict
  DTO decoders, frozen limits (8 MiB frames, 64 KiB externalization
  threshold, 256 KiB content chunks, 8 MiB content objects), a frozen
  error table, and a v1 stability contract (only optional fields, enum
  values, and methods may be added within the major). Provider DTOs are
  public copies of the provider shapes — internal types never cross the
  wire, and credentials never appear in any DTO.
- internal/extension/protocolgen + cmd/extension-protocol-gen: generates
  the canonical JSON Schema (draft 2020-12, sha256:821c11cc…), the schema
  hash constant, and docs/EXTENSION_PROTOCOL.generated.md; -check enforces
  drift-free artifacts and a determinism test double-generates byte-equal
  output.
- internal/extension/sidecar: the only sidecar launch path. Authorization
  invariant: runtimes start only from installed+enabled plugin state —
  project configuration can never declare a sidecar. Exec-form spawn (no
  shell, ${REASONIX_PLUGIN_ROOT} expanded, absolute command required) with
  unfiltered inherited environment (the full-trust contract), 16 KiB
  redacted stderr diagnostics, initialize-first handshake with capability
  subset validation (subscriptions/replaces/providers/UI beyond the
  manifest fail with capability_not_declared, version mismatch with
  unsupported_version), bounded shutdown (shutdown RPC → stdin close →
  750ms grace → process-tree kill → 5s reap), crash supervision that
  cancels pending RPCs and fails later calls fast, a host content store
  with externalization and chunked host/content/read, and per-point
  intercept timeouts (5s input/tool/permission, 30s slow family, manifest
  override clamped to 60s).
- internal/extension SessionExtensions: per-session private plugin state
  at <session>.extensions.json, keyed by plugin ID, 1 MiB per-plugin cap,
  atomic writes, corrupt-file tolerant, never touches the session JSONL.
- boot: BuildResult.Extensions carries the sidecar Manager; the RuntimeSet
  is chained into controller cleanup (like LSP) so sidecars live exactly
  as long as their controller generation and Rebuild retires only the old
  set; required-runtime start failures and replacement-slot conflicts fail
  the build, optional failures degrade to warnings. No runtime packages
  installed means zero sidecars and a byte-identical golden surface.

Verification: go test ./internal/extension/... (sidecar also -race) and
go test ./internal/boot pass incl. golden baselines; extension protocol
generator -check green; re-exec fake-sidecar tests cover handshake
variants, capability violations, early-traffic poisoning, bounded
shutdown, mid-intercept crash, content-ref round trips, stderr redaction,
and boot end-to-end start/stop; gofmt/go vet clean; no new dependencies.
2026-08-03 19:54:56 +08:00
SivanCola 727ac27608 feat(extension): Plugin Manifest v1 with prompts, plugin themes, and full-trust install display
Stage 4 of the unified extension kernel / Extension Protocol v1 plan.

Problem: reasonix-plugin.json had no way to declare prompts, themes, or a
code runtime, no strictness against typos, and no path-safety beyond
lexical checks; install previews gave no signal that a code extension runs
with full trust.

Fix:
- Manifest v1 (apiVersion reasonix.io/plugin/v1): strict decoding with
  DisallowUnknownFields at every level so typos fail with the field path;
  unknown major versions rejected. v1 may mix legacy top-level fields with
  contributes (skills/agents/commands/prompts/hooks/mcpServers/themes):
  identical paths dedupe, same-key-different-definition is a manifest
  error naming the key. Prompts are a new path-list resource exposed as
  /<plugin>:<name> via the existing plugin-command semantics; themes are
  root-confined globs matched against .reasonix-theme files. v1 path
  safety rejects traversal, absolute paths, symlink escapes, and
  non-regular theme files; missing paths are warnings. Manifests without
  apiVersion parse byte-for-byte as before (legacy compat gates green).
- runtime declaration: exec-form command/args/env (never shell-interpreted)
  with required/priority/intercepts/replaces/capabilities validated against
  the extension kernel's interceptor points, replacement slots, priority
  bounds, and {interceptors,strategies,providers,ui}. pluginpkg cannot
  import internal/extension (extension->hook->pluginpkg cycle), so the
  validator lists are duplicated with a sync test pinning them equal.
- Full-trust disclosure: install plans carry RiskHigh + a FULL TRUST
  reason plus omitempty RuntimePlanInfo (command, intercepts, replaces,
  capabilities) that old consumers ignore; CLI plugin show/doctor and
  capdiag render prompts/themes counts and the FULL TRUST block; the
  Desktop install preview renders a danger-styled FULL TRUST section with
  the command line, interceptors, replacement slots, capabilities, and the
  session/environment/permission risk statement. Install/update/replace/
  --link continue to imply full trust with no second confirmation.
- Desktop plugin themes: plugin themes are discovered from enabled v1
  packages, validated by the existing v2 theme validator, and shown
  read-only (never copied into the user theme dir) with IDs
  plugin:<plugin>:<theme>; the internal pack ID regex is unchanged.
  Activating a plugin theme persists the full ID; when the plugin is
  disabled or uninstalled, rendering falls back to the base style while
  the ID is preserved byte-for-byte in desktop-theme-state.json so a
  reinstall of the same plugin restores the theme (non-plugin unknown IDs
  keep the old clear-and-save behavior; legacy state files load
  unchanged). Theme assets serve through the existing content-addressed
  route with digest re-verification.

Verification: go test ./internal/pluginpkg ./internal/installsource
./internal/capdiag ./internal/cli ./internal/extension pass (legacy
compat gates unmodified); cd desktop && go test . passes (plugin theme
discovery/fallback-preserve/restore/read-only/asset-route tests);
desktop frontend pnpm typecheck and pnpm test pass; docs/THEME_PACK.md
updated in both languages; gofmt/go vet clean; no new dependencies.
2026-08-03 18:22:10 +08:00
SivanCola 41a5c82b38 test(extension): record pre-refactor golden baselines and legacy compat gates
Stage 1 of the unified extension kernel / Extension Protocol v1 plan.

Problem: the extension kernel rebuilds runtime assembly (tools, skills,
commands, hooks, MCP, providers) behind a Builder that freezes an immutable
RuntimeSnapshot. Before touching that assembly chain we need executable
proof of the current provider-visible surface, because any byte of drift in
the system prompt, tool schemas, or request serialization cold-starts the
provider prompt cache for every session.

Fix: add regression gates only; no production code changes.
- internal/boot TestGoldenBaselineNoExtensions: committed goldens for the
  composed system prompt (machine-independent: environment probes disabled
  in the fixture config, workspace root normalized to <ROOT>), the
  provider-visible tool contract via the canonical schema path, a synthetic
  provider request serialization, and the agent.PrefixShape cache hash.
  Regenerable via REASONIX_UPDATE_GOLDEN=1 for reviewed, deliberate drift.
- internal/pluginpkg legacy manifest + plugin-packages.json round-trip
  gates: manifests without apiVersion parse exactly as before (unknown
  fields ignored, paths sorted, autoStart:false normalized to nil, hook
  args presence bit preserved), and state v1 saves keep every non-empty
  field while empty strings stay omitted (omitempty status quo).
- internal/agent legacy session JSONL round-trip: pre-event-log sessions
  load and re-save with all seven messages intact.
- desktop theme state: v1 state migrates to v2 on save, user theme IDs
  round-trip, base-style IDs are never persisted, garbage files fall back
  to the default state.

Verification: go test ./internal/boot ./internal/pluginpkg ./internal/agent
and cd desktop && go test . -run TestLegacyThemeState pass;
./scripts/cache-guard.sh passes (all cases tail_avg >= 92, threshold 90).
2026-08-03 12:46:13 +08:00
SivanCola 72a1e32b49 Fix cross-platform plugin Hook execution contracts / 修复跨平台插件 Hook 执行契约 (#6911)
* Fix plugin hook execution contracts

Problem: Windows plugin hooks with compound commands or nested quoting could still fail after the narrow quoted-batch compatibility fix, while explicit empty args could lose exec-form semantics across JSON round trips.

Root cause: Hook launch behavior was inferred from argument length and platform shell heuristics instead of preserving the manifest's exec-form or shell-form contract. CMD and PowerShell command strings also passed through incompatible quoting layers.

Fix: Preserve args field presence and shell selection through plugin parsing and loading, add explicit legacy/exec/shell execution modes, transport raw CMD and encoded PowerShell scripts without token re-rendering, retain the existing simple batch compatibility path, and document the cross-platform contract.

Verification: Focused Hook and plugin package tests, race and shuffled tests, fuzzing, root and Desktop full suites, go vet, and Windows/Linux cross-compilation.

* Fix Windows hook output regressions

Problem: The new compound CMD regression expected cmd.exe to strip quotes from %1, leaving the Windows job red. PowerShell hook scripts also preserved UTF-16LE input transport without forcing captured output to UTF-8.

Root cause: The test confused %1 with the dequoting %~1 form, and the Hook-specific EncodedCommand path did not reuse the shell runtime's existing PowerShell output-encoding prologue.

Fix: Align the CMD assertion with native batch semantics, share the PowerShell UTF-8 script wrapper across shell and Hook launchers, and exercise Chinese plus emoji output in the Windows runtime test.

Verification: Focused Hook/plugin/sandbox tests and vet; race/shuffle count 3; root and Desktop full suites; full vet; Windows amd64 test cross-compilation; PowerShell encoding fuzzing.
2026-07-24 23:07:09 +08:00
SivanCola 7fe7d2b3f3 Simplify MCP authorization for installed servers / 简化已安装 MCP 授权 (#6767)
* fix(mcp): trust installed readers in subagents

Problem:
User-installed MCP tools that declared readOnlyHint could be exposed as readers but then fail before dispatch in planners and read-only subagents. Users were told to configure a second per-tool reader policy after already installing or authorizing the server.

Root cause:
Strict reader trust did not treat explicit install or an exact project launch grant as authority for non-destructive server metadata. Direct, lazy, and on-demand paths also duplicated cached-to-live safety reconciliation, while trusted_read_only_tools remained wired through configuration and Desktop contracts.

Fix:
Authorize non-destructive readOnlyHint tools for installed or launch-authorized servers, carry reader intent only for trusted readers, and share one cached-to-live safety reconciliation boundary across lazy and on-demand adapters. Remove trusted_read_only_tools from runtime, Desktop, examples, and docs while accepting and dropping legacy input on the next save.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && go test -count=1 ./...
- cd desktop/frontend && pnpm test:typecheck
- cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
- ./scripts/cache-guard.sh
- ./scripts/check-cache-impact.sh
- git diff --check

* refactor(mcp): simplify installed server authorization

Problem:
User-installed MCP servers still depended on official-catalog and per-tool reader trust layers, so direct, lazy, and on-demand paths could disagree and strict child sessions could reject an already authorized reader.

Root cause:
Server authorization, signed package status, cached reader trust, and legacy plan-mode allowlists were modeled as separate overlapping decisions.

Fix:
Remove the official MCP catalog, signature/revocation plumbing, package verification, per-tool cached trust, and obsolete reader settings. Use one server-level authorization source for user installs, host-session installs, and exact project grants while retaining live read-only, destructive, and capability-fingerprint checks.

Verification:
- go test ./internal/plugin ./internal/agent ./internal/boot ./internal/config ./internal/installsource ./internal/pluginpkg ./internal/mcplaunch ./internal/planmode
- go test ./internal/acp ./internal/agent ./internal/plugin ./internal/boot
- cd desktop && go test ./...
- ./scripts/cache-guard.sh
- git diff --check

* refactor(mcp): make install authorization final

Problem:
Installed MCP servers could still encounter server, per-tool, writer, or destructive approval branches, and users had to understand settings that duplicated the install decision.

Root cause:
MCP authorization, cached reader trust, approval policy, and process sandboxing evolved as separate paths across direct, lazy, and on-demand execution.

Fix:
Treat install or exact project identity confirmation as the server authorization boundary, keep only explicit global deny plus Plan/read-only and sandbox safety checks, remove catalog and approval configuration, and unify the persistent MCP process sandbox.

Verification:
env -u DEEPSEEK_API_KEY go test ./...
go build ./...
cd desktop && go test ./...
cd desktop/frontend && pnpm typecheck
cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts
cd desktop/frontend && pnpm exec tsx src/__tests__/decision-surface.test.tsx

* chore(mcp): remove retired cache helper

Problem:
CI lint found writeBoolKV left unused after per-tool reader trust was removed.

Root cause:
The cached reader-map fingerprint was deleted, but its serialization helper remained.

Fix:
Remove the dead helper.

Verification:
go test ./internal/plugin
go vet ./internal/plugin
go build ./...

* refactor(mcp): stop blocking schema-only drift

Problem:
Authorized MCP calls could be rejected when a cached tool schema differed from the live server even though authorization and safety classification were unchanged.

Root cause:
A capability fingerprint mixed schema compatibility with read-only and destructive execution safety across direct, lazy, and on-demand paths.

Fix:
Remove the tool-level schema fingerprint chain, keep live server authorization and read-only/destructive reclassification checks, and refresh changed schemas for the next session without interrupting the current authorized call.

Verification:
- go test ./internal/plugin ./internal/agent ./internal/tool
- env -u DEEPSEEK_API_KEY go test ./...
- go vet ./...
- go build ./...
- ./scripts/cache-guard.sh
- pnpm --dir site test
- cd desktop && go test ./...

* fix(mcp): preserve short Windows temp paths

Problem:
Windows stdio MCP servers inherited a deeply nested Reasonix private TEMP path. Servers such as MATLAB create watchdog Unix sockets below TEMP and exit before initialization when the path exceeds the platform limit.

Root cause:
MCP private-state preparation redirected TMP, TEMP, and TMPDIR below the workspace-scoped mcp-state directory on every platform even though Reasonix does not currently provide an OS sandbox on Windows.

Fix:
Keep the host temporary-directory environment for Windows stdio MCP processes while retaining private cache and state directories. Preserve the existing private temporary directory behavior on other platforms and add focused regression coverage for both branches.

Verification:
- go test ./internal/plugin -count=1
- GOOS=windows GOARCH=amd64 go test -c ./internal/plugin
- go test ./...
- go vet ./internal/plugin
- git diff --check

* Simplify MCP runtime authorization state

Use Spec.Authorized as the sole resolved server authorization result so installed and host-session MCP servers no longer depend on launch-manager presence.

Keep exact identity grants for project sources, align planner and strict read-only registries with non-destructive readOnlyHint metadata, and document the cooperative trust boundary.

Add source and agent-surface matrix coverage while preserving the main provider-visible MCP tool surface.

* Limit MCP identity digests to project servers

Problem: User-installed and host-session MCP servers still resolved and hashed their executable identity during every startup even though installation was already the authorization decision.

Root cause: Project launch authorization and schema-cache ownership shared generic fingerprint terminology, and start() computed the project identity before checking whether launch approval was required.

Fix: Skip project identity resolution for installed servers, remove dead and non-participating identity state, and rename the remaining concepts to ProjectLaunchIdentityDigest and SchemaCacheKey while preserving existing JSON keys and digest bytes.

Verification: go test ./internal/mcplaunch ./internal/plugin ./internal/capability ./internal/boot ./internal/control ./internal/agent; go test ./...; cd desktop && go test .; go vet ./...; ./scripts/cache-guard.sh; git diff --check.

* Restore MCP 0.53 compatibility surfaces

Problem: MCP authorization was simplified, but the current client still lacked four compatibility surfaces present in the 0.53 line: legacy SSE, workspace roots, progress notifications, and registry browsing/install.

Root cause: the Go transport abstraction dropped inbound notifications and rejected legacy SSE; runtime specs did not carry a separate workspace root; the registry UI and CLI had no official MCP Registry client.

Fix: add inbound request/progress routing across transports, implement legacy SSE with same-origin endpoint protection, advertise roots/list, and add an explicit cached official Registry marketplace to CLI and desktop. Remove stale approval documentation and the dead helper that broke lint.

Verification: go test ./...; focused go test -race for plugin/registry/CLI/control; go vet in root and desktop; desktop go test ./...; frontend production build and marketplace UI test; Windows amd64 cross-build; official Registry browse smoke test.

* Avoid MCP progress capacity overflow

* Align MCP authorization documentation

Problem: CLI help and Chinese documentation still contained ambiguous or obsolete wording that implied installed MCP writer or destructive calls could require per-call approval.

Root cause: The server-level authorization simplification removed MCP approval modes, but not every user-facing explanation was updated consistently.

Fix: State explicitly that install or exact project identity confirmation authorizes the server, that writer and destructive hints do not prompt per call, and that deny, Plan, and strict read-only filtering remain separate boundaries.

Verification: go test ./internal/cli; go vet ./internal/cli; reasonix mcp help smoke test; stale-wording scan; git diff --check.

* Harden MCP registry installs and SSE replies

Problem:
Registry cache freshness was shared across queries, cached entries could flow into install paths, and legacy SSE server requests spawned an unbounded reply goroutine per request.

Root cause:
The cache stored one global timestamp, Resolve reused Search's offline fallback, the desktop installed browse payloads directly, and the SSE reader delegated every reply to a new goroutine.

Fix:
Track freshness per Registry query with legacy-cache compatibility, require live resolution before CLI and desktop installs, and serialize SSE replies through a bounded single-worker queue with finite deadlines.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- env -u DEEPSEEK_API_KEY go test -race -count=1 ./internal/mcpregistry ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./... (desktop)
- env -u DEEPSEEK_API_KEY go vet ./... (root and desktop)
- pnpm run test:all
- pnpm run build
- env -u DEEPSEEK_API_KEY ./scripts/cache-guard.sh
- git diff --check

* Reject unavailable live registry installs

Problem:
A Registry entry could become non-installable between browsing and the desktop's live re-resolution, leaving the generic add flow with an invalid empty connection. The SSE concurrency regression also used a fixed delay after routing the client response.

Root cause:
The desktop resolver verified freshness but not the resolved entry's installability, and the test inferred worker startup from elapsed time.

Fix:
Validate the live entry through PluginEntry before returning it to the installer, and synchronize the SSE test on the first reply POST instead of sleeping.

Verification:
- env -u DEEPSEEK_API_KEY go test -count=5 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -race -count=3 ./internal/plugin
- env -u DEEPSEEK_API_KEY go test -count=1 ./...
- cd desktop && env -u DEEPSEEK_API_KEY go test -count=1 ./...
- git diff --check
2026-07-22 01:58:43 +08:00
SivanCola c636f017a8 feat(mcp): enforce trusted execution boundaries 2026-07-15 13:55:34 +08:00
SivanCola 0d5f70401b fix(plugins): polish hook adapter semantics and deflake spawn tests
- An unbounded Reasonix wait now omits TaskOutput's optional timeout
  instead of claiming 0 — in Claude's schema timeout is the maximum wait
  in ms, so 0 would read as "don't wait", the opposite of the call.
- Structural input gaps (WebFetch prompt, NotebookEdit cell_number,
  TaskOutput multi-job) are reported once per hooks file instead of once
  per hook item, so a wildcard-matcher plugin gets one warning per gap
  rather than one per hook; file-level wording also keeps the emitted
  set deterministic across the random event-map iteration order.
- Real-spawn hook tests share one generous realSpawnTimeout: node cold
  starts and first-run scans of a freshly built test binary can stall
  for seconds on a loaded machine, and these tests assert behavior, not
  latency. The timeout-path case keeps its tight budget on purpose.
2026-07-15 02:24:50 +08:00
SivanCola c0c93610e6 fix(plugins): complete Claude hook contract adapters 2026-07-15 01:33:36 +08:00
SivanCola 347fd46f85 fix(plugins): flag Claude hook matchers that can never fire
Fourth review round on #6434 found two P2 accuracy gaps once the two P1
security bypasses were confirmed fixed:

- claude_compat.go accepted a hook's matcher without checking whether it
  names a real Claude tool Reasonix has no equivalent for. "WebSearch"
  (no Reasonix web-search tool) and "ExitPlanMode"/"EnterPlanMode" (plan
  approval is a controller-level decision, never dispatched through the
  tool-call hook path; confirmed by tracing permissionRequestHookPayload's
  explicit exclusion of planApprovalTool) are two concrete, independently
  verified examples: a hook scoped to only these matchers would never fire
  while the package still reported "full" compatibility. A plain
  tool-name matcher, or a "|"-alternation of them, naming only tools from a
  conservative, individually-verified unsupported set now downgrades the
  package to "partial" with a structured warning; matchers using other
  regex syntax, or naming even one tool Reasonix does support, are left
  unevaluated to avoid false positives. Also fixed a real gap found while
  auditing this: "run_skill" (Reasonix's skill-invocation tool) was never
  mapped to Claude's "Skill" tool name, so a "Skill"-scoped hook silently
  never fired either — same bug shape as the task/ask fixes from the
  previous round.
- Added a regression test exercising the opts.fresh-only branch of the
  fresh-human approval fix (PlanModeReadOnlyCommandApprovalTool, which is
  fresh via requestFreshApprovalDecision without being one of
  RequiresFreshHumanApprovalTool's fixed cases) — the existing tests only
  covered tools protected through requiresFreshApprovalTool(tool).

Co-authored-by: SivanCola <[email protected]>
2026-07-14 09:30:28 +08:00
SivanCola 19e25a9259 fix(plugins): stop hook auto-allow from bypassing fresh-human approval
Third review round on #6434 found a real regression from the prior fix, plus
two remaining accuracy gaps:

- [P1, security] A Claude PermissionRequest hook's "allow" decision (added in
  the previous fix) was honored unconditionally, including for tools that
  RequiresFreshHumanApprovalTool deliberately excludes from YOLO/auto-approval
  and Guardian: sandbox escapes, Reasonix config writes, and memory
  remember/forget. A broadly-matched plugin hook returning allow could
  silently rubber-stamp any of those. Auto-allow is now ignored for
  fresh-human-required tools (falls through to the normal interactive
  prompt); a hook's deny still applies universally since refusing is always
  safe to auto-honor.
- [P1, security] The Claude subagent tool was renamed "Task" -> "Agent"
  (confirmed against the current https://code.claude.com/docs/en/tools-reference);
  Reasonix's "ask" tool (AskUserQuestion's equivalent) wasn't mapped at all.
  A matcher or tool_name check written against either current name silently
  never fired. Now emits the current name (task -> Agent, ask ->
  AskUserQuestion) while still matching hooks whose matcher uses the legacy
  "Task" alias.
- [P2] Documented, at both the code (compatibilityFor) and doc level, exactly
  what "full" compatibility does and doesn't guarantee: it reflects
  successful static mapping of declared capabilities, not full runtime
  parity — updatedInput and PreToolUse's ask/defer decisions are chosen by a
  hook's stdout at call time and can't be flagged during install the way
  if/asyncRewake/Stop/SubagentStop already are.

Co-authored-by: SivanCola <[email protected]>
2026-07-14 04:06:03 +08:00
SivanCola 741fb83664 fix(plugins): map Claude tool names in hooks and close remaining deny gaps
Second review round on #6434 found two more hook security gaps and an
over-claimed compatibility status:

- imported Claude hook matchers (e.g. "Bash", "Write|Edit") and the
  tool_name field a hook script inspects were left in Reasonix's own
  vocabulary (bash, write_file, ...), so a matcher-scoped or tool_name-gated
  Claude hook silently never fired. Added a name-translation layer
  (claudeFacingToolName) applied only to PayloadFormat "claude" hooks, both
  at match time and in the emitted payload.
- UserPromptSubmit's standard exit-0 JSON block (top-level
  {"decision":"block","reason":...}, distinct from PreToolUse/
  PermissionRequest's hookSpecificOutput shape) was not parsed, so a hook
  denying a sensitive prompt via JSON instead of exit 2 was ignored.
- PermissionRequest's "allow" decision (auto-approve without prompting) was
  never implemented — only the deny half of Claude's auto-answer contract
  from the prior fix. Runner.PermissionRequest now returns a tri-state
  decision (nil/deny/allow) and the approval flow honors all three.
- claude_compat.go now parses the "if" and "asyncRewake" hook fields and a
  Stop/SubagentStop hook import, and downgrades compatibility to "partial"
  with a structured warning instead of claiming "full" for semantics
  Reasonix doesn't evaluate (conditional execution, wake-on-exit-2 async,
  and block-the-turn respectively). The hook is still imported best-effort.

An existing test (TestPlanClaudeCompatibilityReportsAgentsHooksAndMCP,
modeled on the PR's own agent-critter example) had asserted "full"
compatibility for exactly this Stop-hook case; updated to the corrected
"partial" expectation.

Co-authored-by: SivanCola <[email protected]>
2026-07-14 03:38:21 +08:00
SivanCola 6506ba46b7 fix(plugins): honor Claude hook deny decisions and fix hook dedup
Review on #6434 found that an imported Claude PreToolUse/PermissionRequest
hook's deny decision was silently dropped: ParseOutput only ever extracted
additionalContext from hook stdout, and PermissionRequest's exit code 2 was
explicitly downgraded to a notification instead of a block. A Claude plugin
that relies on hooks to refuse dangerous commands or permissions would show
as fully compatible while the tool call still ran.

- honor Claude's JSON deny decision (hookSpecificOutput.permissionDecision
  for PreToolUse, hookSpecificOutput.decision.behavior for PermissionRequest)
  even on exit 0, and have exit code 2 block an imported PermissionRequest
  hook the same way Claude's own contract does. Scoped to PayloadFormat
  "claude" so native Reasonix hook semantics (PermissionRequest stays
  advisory-only) are unchanged.
- wire the PermissionRequest hook synchronously into the approval flow so a
  deny preempts the prompt instead of only notifying after the fact.
- fix hook dedup (appendUniqueHook) to also compare Env, Timeout, Async, and
  Cwd, so two hooks that share a command/matcher but differ in one of those
  no longer collapse into one.
- fix Windows path-separator mismatch: ${CLAUDE_PLUGIN_ROOT}/${CLAUDE_PROJECT_DIR}
  expansion left a mixed "C:\...\pkg/bin/server" string since the manifest's
  "/"-authored suffix was never converted to the host separator.
- lint: tagged switch (QF1003) and remove two functions orphaned by this PR's
  own refactor (appendClaudeSettingsHooks, pluginNameExists).

Co-authored-by: SivanCola <[email protected]>
2026-07-14 02:58:26 +08:00
SivanCola cfe2b80c5c feat(plugins): add Claude plugin compatibility 2026-07-14 01:25:22 +08:00
SivanCola ac2aa357c8 feat(plugin): qualify plugin skill slash names 2026-07-11 18:39:50 +08:00
SivanCola 4e61d66c6f feat(plugin): qualify plugin slash commands 2026-07-11 17:37:53 +08:00
SivanCola feff9f4da5 fix(plugin): pin GitHub plugin installs to the approved commit
Round-3 review follow-ups on the commands mapping:

- P1: plan and apply each cloned the source separately, so a repo that
  moved between the approved plan and the apply resolution could install
  content the approval never covered (and the optional planId echo could
  not catch the apply call's own second clone). pluginSource now reports
  the resolved commit for git sources; the plan records it on the action
  — entering the plan ID, so approvals fingerprint the exact snapshot —
  and apply pins a diverged clone back to the approved commit via a
  full-SHA fetch + detached checkout, refusing with a re-plan error when
  the snapshot is no longer reachable. Regression covers the unpinnable
  drift refusal end to end.
- P2: the command-dir adoption gate capped recursion at 5 levels while
  the runtime loader walks unbounded, so a plugin whose only command sat
  deeper was never adopted. The gate now delegates to internal/command
  itself — adoption and runtime discovery share one implementation and
  cannot diverge; a six-level nesting regression pins it.
2026-07-11 04:58:20 +00:00
SivanCola 2ef71c8165 fix(plugin): plan GitHub plugin installs from the real source tree
Review follow-ups on the commands mapping:

- P1: planGitHubPluginPackage assembled a sparse temp dir holding only
  the fetched manifest, so convention-discovered capabilities (skills/,
  commands/) and their warnings were invisible at plan time while apply
  cloned the full repo — the approved plan under-described what actually
  got installed. Plan and apply now resolve the source through one
  shared pluginSource resolver (shallow clone for GitHub), making the
  approval contract hold by construction; a plan/apply consistency
  regression covers it.
- P2: dirContainsCommandMd only looked at the top level, so a plugin
  whose commands all live in namespace subdirs (commands/git/commit.md)
  was never adopted even though the runtime loader walks them. The
  adoption gate now recurses (bounded depth, hidden dirs skipped).
- P3: appendInventoryText's empty-inventory fallback ignored commands, so
  a commands-only plugin printed its command list followed by
  "capabilities: no detailed inventory available".

Verified: reviewer's repro now plans skills=6 commands=11 for
github.com/OthmanAdi/planning-with-files with zero warnings.
2026-07-11 04:19:24 +00:00
SivanCola d38945513c feat(plugin): map Claude plugin commands to custom slash commands
Claude plugins like planning-with-files ship their primary interface as
commands/ (flat <name>.md prompt templates invoked as /<name>), which
Reasonix previously dropped with a "does not map yet" warning — the
plugin installed but /plan, /pwf etc. never appeared, reading to users
as a failed install (discussion #5762).

A Claude command is exactly Reasonix's custom slash command shape
(internal/command: frontmatter description/argument-hint, $ARGUMENTS /
$1..$N substitution, subdir namespacing), so map the capability instead
of warning about it:

- pluginpkg: Manifest gains Commands (explicit "commands" list in any
  manifest kind; claude plugins adopt conventional commands/ and
  .claude/commands/ automatically, even alongside explicit skills).
  Inventory/CapabilityCounts/describe now report commands, loaded via
  internal/command so names can never drift from runtime behavior.
- config: CommandDirsForRoot prepends enabled plugin packages' command
  roots at the lowest priority — boot and hot ReloadCommands both pick
  them up, and a user- or project-authored command wins name clashes.
- cli/installsource: plugin verify checks command roots; install plans
  carry commands/commandCount; plugin show lists commands.
- desktop: plugin view exposes commands count + details; capabilities
  panel renders a Commands section (en/zh/zh-TW strings).
- docs: EN/zh-CN plugin docs describe the mapping; commands removed
  from the unmapped-capabilities warning list (agents, hooks/hooks.json,
  .mcp.json still warn).

Verified end-to-end against planning-with-files v3.4.0: installs with
zero warnings, 6 skills + 11 commands, /plan invocable.
2026-07-10 17:58:13 +00:00
Sivan 5078a1987f Fix Windows encoded config reads 2026-07-10 02:40:43 +08:00
SivanCola 6a68799e18 fix(pluginpkg): discover conventional Claude skill dirs and warn on unmapped capabilities
Review findings on the Claude manifest support:

- A standard Claude plugin ships a metadata-only plugin.json and relies
  on Claude's auto-discovery of the conventional skills/ directory (packs
  in the wild also use .claude/skills/). Reading only the explicit
  `skills` field installed such packages as zero capabilities with no
  warning — "install succeeded" while nothing usable landed. Claude-kind
  parsing now falls back to the conventional directories when the
  manifest declares none, adopting a directory only if it actually holds
  a <name>/SKILL.md. Explicit declarations disable the fallback; codex
  parsing is untouched.
- Conventional Claude capabilities Reasonix does not map yet (commands/,
  agents/, hooks/hooks.json, .mcp.json) now surface as install warnings
  instead of being silently dropped.
- Negative test pins the includeCodexSessionStartHook boundary: a
  claude-kind package shipping hooks/session-start-codex must not get it
  registered as an executable SessionStart hook.
- Docs: retitle the compatibility section to "Codex & Claude", document
  the fallback, the warnings, and that marketplace.json indexes are out
  of scope.

Fallback reverse-verified: removing the convention-dir call fails
TestParseClaudePluginConventionSkillDirs.
2026-07-07 15:58:05 +08:00
SivanCola c070dc7432 Support Claude plugin manifests 2026-07-07 15:25:59 +08:00
SivanCola 885c7886ef fix(fileutil): chmod tmp before close to survive Windows sharing races
AtomicWriteFile chmod'd the temp file by path after closing it, leaving a
window where the Windows AV / search indexer could grab or move it and make
the chmod fail with 'file not found' — observed flaking the concurrent
pluginpkg state test on windows-latest. Chmod the still-open handle instead.
The reader-side test now retries the transient sharing violation the publish
rename can trigger on Windows (a real torn file still fails via SyntaxError).
2026-07-05 00:25:12 +08:00
SivanCola bd7d0fcd67 fix(plugin): budget stdio waits and stop probing shells under the lock
withStderr reaped the child with an unbudgeted cmd.Wait while holding callMu:
a surviving grandchild that keeps the stderr pipe open wedged every future
call on the transport (close() already budgets exactly this wait). Share its
5s budget via waitWithBudget. cachedShellPATH ran up to ~6s of login-shell
probes under its lock and re-probed empty results on every spawn; probe
off-lock with an in-flight channel and cache completed empty results.
pluginpkg state RMW gets a process lock and atomic writes.
2026-07-04 23:28:50 +08:00
SivanCola 9bc6344fa1 feat: add session plugin usage command 2026-07-03 11:58:21 +08:00
SivanCola 253faa132f feat: surface plugin package usage details 2026-07-03 11:39:17 +08:00
SivanCola ad3d06343d feat: expand Claude-style plugin compatibility 2026-07-03 11:01:47 +08:00
SivanCola 42ee48f214 feat(plugin): add unified plugin packages 2026-07-02 17:23:56 +08:00