docs: delete stale documents, untranslated copies and main-v2 release remnants

Deletes what the documentation standard (6b6a41d36) classes as dead on
the studio line, as reviewed and approved item by item.

Deleted:
- Stale or unreferenced: production_checklist (a "v5.9.9 stable" that
  never existed), the 4-line WINDOWS_LTSC_2019 stub,
  SESSION_REFERENCE_ARCHITECTURE, docs/superpowers (the retired
  autoresearch plans), docs/research, RELEASING.md (main-v2's three-tag
  line), SIGNPATH_WINDOWS_ADMIN_SOP.md (desktop line only) and
  CHANGELOG.md (main-v2's Go line).
- Chinese copies: 30 zh-CN documents with an English original, plus
  three with none (COLLABORATION_MODES, DESKTOP_HOOKS, GOAL_ENFORCEMENT).
  Only README, GUIDE and CLI keep a Chinese page.
- main-v2 release pipeline: the prepare-release-notes, release-notes,
  release-verify-issues and pages workflows; their release-notes,
  generate, event, stable-validation and public-contract scripts; and
  release-notes/releases.json.

What had to move with it:
- releases.json was embedded, so the docs tool served main-v2's
  1.17-1.24 history inside Studio 2.17, where "latest release" answered
  1.24.1. productdocs drops the release catalog: release_notes.go, the
  virtual changelog paths, the version boost and Manifest.ReleaseNotes.
  docs-manifest --verify-source now compares docs/ only. The /docs
  example no longer promises a changelog.
- spec-parity held SPEC.zh-CN.md to SPEC.md. With one language it has
  nothing to compare, so the rule and its tests are removed.
- The spatiotemporal golden fixtures lived under docs/superpowers but
  are test data. They move to internal/extension/testdata.
- Links: onboarding pages point at the English documents; English
  documents lose their switch to a copy that is gone. GUIDE drops the
  pointer to the Chinese-only hooks page. The theme provenance generator
  stops writing THEME_ASSETS.zh-CN.md. ci.yml stops linting and running
  what was deleted. CODEOWNERS and the standard lose the two dead paths.

Checked: no broken relative .md link remains; actionlint passes;
go test ./... passes; make lint is clean.

Baseline only goes down:
- doc-language 33 -> 0
- doc-owner 91 -> 44
- doc-prose 2755 -> 2375
- spec-parity removed

site/ is also main-v2's and still reads releases.json. It was not on the
approved list and is left for a separate decision.
This commit is contained in:
esengine committed 2026-09-16 21:49:47 -07:00
1 parent c37dac3a40
commit 68b031ae5d
65 files changed
-22110

No files matched your search

-47
View File
@@ -1,47 +0,0 @@
name: Deploy site
on:
push:
branches: [main-v2]
paths: ['site/**', 'release-notes/**', '.github/workflows/pages.yml']
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '22'
cache: npm
cache-dependency-path: site/package-lock.json
- name: Build
working-directory: site
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
npm ci
npm run build
- uses: actions/upload-pages-artifact@v5
with:
path: site/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- id: deploy
uses: actions/deploy-pages@v5
-101
View File
@@ -1,101 +0,0 @@
name: Prepare release
on:
workflow_dispatch:
inputs:
version:
description: "Official version without a tag prefix (for example 1.20.0)"
required: true
type: string
permissions:
contents: write
pull-requests: write
jobs:
draft:
name: generate reviewable bilingual draft
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: "22"
- name: Select the review branch
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin main-v2
if [[ ! "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::version must be MAJOR.MINOR.PATCH"
exit 1
fi
branch="release-notes/v${VERSION}"
if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then
git fetch origin "refs/heads/$branch:refs/remotes/origin/$branch"
git checkout -b "$branch" --track "origin/$branch"
git merge --no-edit origin/main-v2
else
git checkout -b "$branch"
fi
echo "RELEASE_NOTES_BRANCH=$branch" >> "$GITHUB_ENV"
- name: Generate and validate release notes
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
DEEPSEEK_MODEL: deepseek-v4-pro
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
node scripts/generate-release-notes.mjs --version "$VERSION"
node scripts/release-notes.mjs validate
node --test scripts/release-notes.test.mjs
node scripts/release-notes.mjs render --version "$VERSION" --output /tmp/release-notes.md
{
echo "## Review draft for v${VERSION#v}"
echo
cat /tmp/release-notes.md
} >> "$GITHUB_STEP_SUMMARY"
- name: Open or update the review PR
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
git add release-notes/releases.json
if ! git diff --cached --quiet; then
git commit -m "docs(release): prepare v${VERSION#v} notes" \
-m "Summary:
Generate a bilingual, product-focused draft from merged pull request metadata. Reuse the selected release-bound PR when one is available.
Verification:
Validate the catalog, citations, bilingual fields, and rendered GitHub release notes before committing."
fi
git push -u origin "$RELEASE_NOTES_BRANCH"
if ! gh pr list --head "$RELEASE_NOTES_BRANCH" --state open --json number --jq 'length > 0' | grep -q true; then
if ! gh pr create \
--base main-v2 \
--head "$RELEASE_NOTES_BRANCH" \
--title "docs(release): 准备 v${VERSION#v} 中英更新日志" \
--body-file /tmp/release-notes.md; then
echo "::warning::GitHub Actions could not open the PR; the reviewed branch is preserved."
{
echo "## Manual PR handoff"
echo
echo '```sh'
echo "gh pr create --repo ${{ github.repository }} --base main-v2 --head $RELEASE_NOTES_BRANCH --fill"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
fi
fi
-26
View File
@@ -1,26 +0,0 @@
name: Release notes
on:
pull_request:
paths:
- "release-notes/**"
- "scripts/release-notes*.mjs"
- "site/src/pages/changelog/**"
- "site/src/components/ChangelogPage.astro"
- "desktop/frontend/src/App.tsx"
- "desktop/frontend/src/components/SettingsPanel.tsx"
- "desktop/frontend/src/components/UpdateBanner.tsx"
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
- run: node scripts/release-notes.mjs validate
- run: node --test scripts/release-notes.test.mjs
@@ -1,62 +0,0 @@
name: Request verification on released issues
# A PR that writes "Fixes #N" auto-closes its issue on merge. One that writes
# "Refs"/"Related"/"Addresses" does not, so the report stays open waiting for a
# follow-up nobody remembers to send after the release. This sends it.
on:
workflow_dispatch:
inputs:
tag:
description: Release tag to announce (e.g. desktop-v1.18.0)
required: true
type: string
dry_run:
description: Print the targets without commenting
required: false
default: true
type: boolean
release:
types: [published]
push:
branches:
- main-v2
paths:
- '.github/workflows/release-verify-issues.yml'
- 'scripts/release-verify-issues.mjs'
- 'scripts/release-verify-issues.test.mjs'
permissions:
contents: read
issues: write
concurrency:
group: release-verify-issues-${{ github.event.inputs.tag || github.event.release.tag_name || github.ref }}
cancel-in-progress: false
jobs:
notify:
if: github.repository == 'esengine/DeepSeek-Reasonix'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-node@v7
with:
node-version: '22'
- name: Test notifier
run: node --test scripts/release-verify-issues.test.mjs
# A push that only touched the script has no release to announce; the test
# above is the whole point of that trigger.
- name: Request verification
if: github.event_name != 'push'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.inputs.tag || github.event.release.tag_name }}
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
run: |
set -euo pipefail
args=(--tag "$TAG")
if [ "$DRY_RUN" = "true" ]; then args+=(--dry-run); fi
node scripts/release-verify-issues.mjs "${args[@]}"
-385
View File
@@ -1,385 +0,0 @@
# Changelog
All notable changes to the Go line (Reasonix 1.0+) are recorded here. The legacy
`0.x` TypeScript history lives on the [`v1`](https://github.com/esengine/DeepSeek-Reasonix/tree/v1)
branch.
## Unreleased
### Fixed
- **v1.24.2 session snapshot & recovery root fix:** Keep PR #7982's WAL/CAS/lease
safety foundation, but replace process-level "I hold a lease" ownership with a
generation-bound `SessionWriteAuthority`. Same-revision tool-preview/load
reshapes no longer false-diverge; recovery files are bounded to one path per
writer/lineage; empty checkpoints heal from their own WAL; projection lineage
rebinds across upgrade/model switch and inherits across recovery forks without
changing provider-visible prompt bytes. Catalog upgrades to disposable
`session-catalog/v3.sqlite` with recovery lineage roles
(`normal|covered_copy|adopted|diverged`); covered idle copies move to the
recoverable `.trash` using a 15-minute idle threshold applied on two early
sweeps (at startup and ~20 minutes later), then a 24-hour threshold on the
6-hour background ticker; independent diverged branches stay and are listed
for user choice. v1/v2 catalogs are
left byte-unchanged for coexistence/downgrade.
**v1.24.1** only hid/reclaimed already-created covered copies and fixed Windows
flash-window startup; **v1.24.2** stops the misclassification source and repairs
existing user directories without rewriting authoritative JSONL/WAL/sidecar data.
- Goal now runs continuously by default: the former 16-round per-Run boundary,
10/20/40 cross-Run quotas, default wall-clock budget, and numeric
no-progress/Todo-stall pauses no longer stop valid work. Progress guards still
detect repeated host outcomes and zero-evidence work, but redirect the model
to re-plan instead of producing `goal_run_budget` or `goal_stuck`. Explicit
`[agent].goal_token_budget`, `--max-steps`, positive time/cost budgets, manual
pause/stop, genuine user/external blockers, and evaluator fail-closed behavior
remain available. The Goal token budget defaults to `0` (off); resuming its
`budget_spend` pause grants a fresh slice without clearing cumulative usage.
Goal status reports turns, provider requests, tokens, the optional configured
token threshold, and cumulative active work time. Bot `max_steps` also
defaults to `0` (continuous), while positive user configuration is enforced.
- Removed numeric Goal pauses in existing sidecars automatically normalize to
`running` without sending a model request. Active Goal sidecars write
`turnsLimit: -1` as a downgrade-safe unlimited sentinel while public runtime
APIs retain deprecated limit fields as `0`. The migration preserves unknown
fields, todos, checkpoints, usage, evidence, and historical metadata.
- Goal is now the sole long-task runtime. Historical AutoResearch sidecars
migrate transactionally into Goals with research compatibility metadata. Invalid archives block
fail closed and remain read-only, retaining the task id and compatibility mode
for a restart or `/goal resume` retry; successful Goal-only sidecars omit the
old task id and write an explicit downgrade fence so previous readers cannot
reactivate the removed runtime.
- Context-dependent workflow tools now share one host-side execution boundary.
Goal, Plan sign-off, and background-job calls cannot reach permissions,
hooks, leases, or Execute outside their owning context; mixed batches execute
valid calls once and stop safely after one repair. Child agents also isolate
inherited Goal, Jobs, and live memory queues, while persisted tool identity
records the effective child schema projection.
- **Issue #7575:** Linux Bash under bubblewrap no longer mounts a fresh empty
`--tmpfs /tmp` on every call. Consecutive commands in the same logical session
now share a private temporary directory (bound at `/tmp` on Linux, exported via
`TMPDIR`/`TMP`/`TEMP` on all platforms) without exposing the host public
temporary root. `/new`, `/clear`, resume of another session, and branch
switches rotate the directory; model/settings hot rebuilds keep it. Sub-agent
runs get independent directories. Temporary files are not durable across process
restarts.
### Added
- **Remote SSH: model credentials stay on your machine.** A remote session now
resolves its providers over a reverse (`-R`) forward back to a broker Reasonix
runs on this machine's loopback, so the remote host needs no API key of its
own and no egress to the model API. The key is never written to the remote
disk — only a per-connection broker token, 0600. The provider runs here, so
every endpoint-dependent judgement (DeepSeek's beta prefix endpoint, Kimi's K3
contract, Gemini model resource names, MiniMax's reasoning split) reads a real
vendor URL, and typed provider errors keep their identity across the tunnel:
a remote turn rejected for context overflow still classifies as one and still
compacts. Per-host `provider = "local" | "remote"` picks which side answers,
defaulting to `local`; `reasonix remote serve start` never uses a broker,
since that serve outlives the command that published it.
- **Remote SSH: the host fetches its own kernel.** `serve_install = "auto"` now
tries, in order: the remote downloading the release over its own connection
(Reasonix resolves `SHA256SUMS` here and hands over the URL and digest, so the
host is not verifying two things the same network gave it), uploading this
machine's binary, downloading here and pushing over SFTP, and finally npm.
npm was previously first and required Node ≥18 on the far side to deliver the
identical static Go binary. A first connect to a Linux/macOS host with only
sshd now transfers roughly 21MB over that host's own link instead of pushing
62MB over the user's uplink. `reasonix remote test` reports which routes are
open and names what closes each one that is not.
- Added `[ui].show_turn_usage` so CLI/TUI users can hide per-request token and
cost receipts from transcript scrollback without disabling usage accounting.
## [1.20.0] — 2026-08-05
Extension kernel, Task Monitor, and safer Goal completion.
Compact decision surfaces, local decision receipts, unified extension kernel,
native Task Monitor, bounded sub-agent progress, Goal fail-closed completion,
MiMo and DashScope Responses fixes, SSH remote access simplification, and
multiple Desktop stability improvements.
### Highlights
- **Unified Extension Kernel and Extension Protocol v1**: Immutable runtime
snapshots, fail-atomic reload, Plugin Manifest v1 (prompts, themes, full-trust
code runtimes), stable JSON-RPC sidecar protocol, interceptor dispatch,
streaming provider adapter, structured UI, and Go SDK.
- **Native Task Monitor**: Monitor agent tasks natively in CLI and Desktop with
lifecycle semantics and session-scoped summary view.
- **Bounded Sub-agent Progress Forwarding**: Forward structured progress for
`task`, `parallel_tasks`, and `fleet` without flooding the parent stream.
Renders nested lifecycle cards in Desktop and stable per-child transcript
slots in CLI.
- **Goal Completion Fail-Closed**: Replace free-form Goal footer markers with a
stable `update_goal` tool and epoch-scoped per-turn reports. Centralized
completion logic with bounded evaluator, progress-aware budgets, and
pause/resume controls.
- **Ablation Subsystem Switches**: Switch subsystems off behind one shared
vocabulary for controlled experiments. Includes planner, subagent, retrieval,
evidence, and compaction.
- **Benchmark Cost per Solved Task**: Report cost per solved task, tokens per
solved, median wall time, and failure-class breakdown in e2e reports.
- **Compact Decision Surfaces and Local Receipts**: Compact footer decision-card
layout with bounded scroll, dense action rows, and overflow disclosure.
Record bounded Ask, approval, and recovery decisions as local transcript
receipts.
- **Simplified SSH Remote Access**: Remove Remote Workbench protocol and
stacks; reuse CLI/Serve remote model. Desktop opens per-host native web child
windows via SSH. Keyless remote Serve setup with loopback-only page.
- **Model Usage Charts with Primer Palette**: Replace monochrome accent ramp
with GitHub Primer data-viz two-set categorical palette. Fix donut overflow
on hover and keyboard accessibility.
- **Cross-platform Extension and Task Monitor Reliability**: Make
content-reference eviction deterministic, reject Unix and Windows absolute
plugin paths consistently, stabilize parallel-task cancellation, and restore
reliable Windows validation for Task Monitor and remote provider setup.
- **MiMo and DashScope Responses Wire Alignment**: Fix multi-turn tool loops,
reasoning round-trip, JSON output for MiMo; fix DashScope second-turn 400
error, all-zero usage suppression, and vendor-aware cache TTL.
- **Desktop Stability Fixes**: Recover stuck updates and legacy WebKit, contain
macOS alias repair startup crashes, keep composer overflow stacks readable,
and harden account verification and community flows.
- **Remote Web Recovery After SSH Drops**: Add integration regression test for
SSH drop, forward recovery, and window reload. Document transient outage
behavior.
- **CI: Auto-minimize Activity-Farming Spam Comments**: Detect and minimize
template spam comments from non-contributor accounts based on structural
signals.
### Added
- Added Extension Protocol v1 and the unified extension kernel: installed or
linked sidecars can contribute tools, skills, commands, hooks, MCP servers,
providers, interceptors, and structured UI surfaces through a versioned
NDJSON contract and the public Go SDK. CLI, Desktop, ACP, and Serve support
fail-atomic runtime reloads; Serve also renders extension surfaces and lists
extension-hosted providers without exposing credentials.
- Added the structured Goal completion protocol: the always-registered
`update_goal` tool (continue/complete/blocked with reason and next_action)
replaces the `[goal:*]` footer markers. The Goal FSM is now the exclusive
cross-turn decision point and validates every complete claim against Delivery
readiness; when the model submits no report, an independent bounded evaluator
(recovery_model → guardian_model → main model, no tools/history, usage
attributed to `goal-evaluator`) judges the turn once, and any evaluator
failure pauses the goal instead of continuing silently.
- Added Goal budget classes with safe pauses: simple 10 turns / 200k tokens,
write 20 turns / 400k tokens, AutoResearch 40 turns / 800k tokens, and a
4-turn no-host-verifiable-progress gate. Pauses keep all Goal state; `/goal
resume` continues and adds one slice of the current class when the pause was
budget-related. `/goal status` shows the full turn/token/no-progress runtime,
and `/goal pause` manually suspends a running Goal.
- Added the `goalRuntime` nested view to the desktop Meta, the remote protocol
(`session/goal/pause` operation, `goalRuntime` DTO on session meta), and the
ACP status payload; the desktop Composer goal menu shows the runtime summary
with distinct pause/end/resume actions.
### Changed
- Delivery no longer retries final-answer readiness with hidden model messages:
a plain Delivery run ends on the first unsatisfied final answer and surfaces
the recovery card, while a Goal + Delivery run has the Goal FSM absorb the
failure and continue under budget with the missing requirements as the next
turn's prompt. Historical `[goal:*]` footers are stripped from old transcripts
for display only and never participate in state decisions.
- Added a **Remote SSH** module (VS Code Remote-SSH style): a user-global
`[remote]` host config, `reasonix remote` CLI (add/list/remove/import/test/
connect/status/forward/serve/fs) and `/remote` slash command, an SSH transport
with trust-on-first-use host-key verification, keepalive + exponential-backoff
reconnect, `-L`/`-R` port forwarding, and SFTP file access. `connect`
bootstraps a persistent `reasonix serve` on the remote host and tunnels its
loopback port so the full agent runs remotely. The desktop app adds a
**Settings -> Remote SSH** host manager, a remote file browser/editor, a
port-forwarding panel, and a status-bar connection chip. Linux/macOS remotes.
- Added `reasonix serve --port-file/--token-file/--pid-file` so a supervised
headless serve can bind an ephemeral port and read its auth token from a file
(keeping it out of `ps`).
- Added an authenticated, loopback-only Provider setup page for `reasonix
serve`. A Serve whose selected Provider is missing its API key now remains
reachable, stores the submitted key in that host's Reasonix credential file,
and rebuilds the active controller in place without restarting Serve.
- Added Claude Code-style searchable CLI pickers for models, providers, and
sessions, with arrow, Vim, and `Ctrl+P` / `Ctrl+N` navigation.
- Added `-p` / `--print`, `text`, `json`, and `stream-json` output modes for
one-shot use and automation.
- Added session-scoped `--allowed-tools`, repeatable `--add-dir`, Claude-compatible
permission modes, flexible `--resume [QUERY]`, and the `--copy` resume escape
hatch.
- Added `/status` details for the active model, effort, cache, Git state,
background jobs, work profile, and provider balance where available.
- Remote SSH workspaces now open as a standalone remote web window again.
Opening a workspace from the status bar or the Remote Server tab starts or
reuses the remote `reasonix serve`, tunnels its loopback port, and opens the
Serve web client in a dedicated per-host window. The remote web page uses
the provider configuration and API keys on the **remote** host; the desktop
no longer exposes its local providers to remote hosts. If the selected remote
Provider is missing its API key, the window opens a setup page that saves the
key only on that host and then opens the normal Serve UI. The Remote Workbench
protocol, its Provider Broker, and the same-window remote projection were
removed. Legacy mirror and provider-trust files are not deleted
automatically; Settings -> Remote SSH shows a cleanup card when they exist.
The hidden `remote attach-workspace`, `remote runtime-workbench`, and
`remote workbench-build-id` commands now fail with a pointer to
`reasonix remote connect <host> --open`.
- Automatic Plan Mode has been retired. Plan Mode is now always entered through
an explicit user choice, and the one-time config v5 upgrade removes legacy
`agent.auto_plan` and `agent.auto_plan_classifier` values so upgraded users
receive the same behavior as new users.
- `Shift+Tab` now cycles CLI safe modes from Ask to Auto to Plan, while YOLO
remains an independent `Ctrl+Y` toggle.
- Model, provider, resume, and approval menus now use consistent row selection;
slash completion, help, aliases, and dispatch share one command registry.
- The full-screen CLI composer now uses theme-accented borders and a slim bar
cursor by default, grows within the available terminal height, scrolls long
drafts independently, and preserves selections across explicit image paste.
- The persistent CLI footer now uses a responsive, theme-aware layout for
interaction state, model, effort, localized work mode, Git identity, cache,
context, compaction headroom, jobs, and balance. Narrow terminals move or
compact complete groups instead of clipping labels.
- CLI clipboard actions now separate terminal-native text paste from explicit
image paste: `Ctrl+V` on macOS/Linux, `Alt+V` on Windows, or `/paste-image`.
Local transcript copy verifies the native clipboard write, while SSH uses a
clearly labelled OSC 52 fallback.
- Runtime rebuilds after model, effort, or work-mode changes now preserve the
conversation, session permission overrides, additional directories, and
session lease ownership.
- Agent execution now monitors host-observed Todo progress automatically. A
stalled current item receives a recovery nudge after 8 tool-call rounds with
no new completion, unique read, command, or mutation, and pauses with saved
work after 16. Exact repeats do not renew the progress lease; real work does.
Two-level task lists keep the single in_progress contract: the active
sub-step is the only current item while its phase stays pending, and the
phase becomes in_progress to sign off only after all of its sub-steps are
completed. A level-1 sub-step with no phase header above it is rejected.
Executor and planner rounds now use automatic progress management. Retired
`[agent].max_steps` and `planner_max_steps` keys remain parseable for upgrades,
but are ignored and removed by a one-time migration so stale hidden limits
cannot truncate new behavior. One-off CLI and unattended bot limits remain.
### Fixed
- Fixed long parallel sub-agent research being silently lost when combined
`parallel_tasks` or `fleet` answers exceeded the single-tool output limit.
Persisted sessions now keep each child transcript independently, return a
bounded fair preview plus stable reference for every result, and page full
answers through the conversation-scoped `read_subagent_result` tool.
- Fixed Remote Workbench failing with only `initialize: workbench-desktop:
connection closed` on fresh or cross-platform SSH hosts. Desktop now proves
the exact Host CLI Build ID, provisions the matching verified release without
requiring remote npm, runs the managed binary explicitly, and preserves a
safe structured bootstrap error when the remote command exits early.
- Hardened Bash permission reuse for dynamic and indirect execution. Parameter/arithmetic expansions,
assignments, redirects, heredocs, and globs can only be remembered as exact
`Bash=<literal>` rules, while still using Auto's normal fallback. Nested or
indirect execution now requires a human in interactive Ask/Auto and fails
closed in headless Ask/Auto/DontAsk. Broad Bash rules, Guardian/hook allows,
and the approved-plan window can no longer silently authorize that stricter
class; YOLO remains the explicit full-access bypass and sandbox enforcement
is unchanged.
- Fixed Desktop sessions incorrectly locking themselves during Goal + Delivery
mode changes, controller rebuilds, duplicate-tab restore, and background
reattachment. Desktop now keeps one process-local runtime owner per canonical
session, fences stale controller events by runtime epoch, blocks sends until
that runtime is ready, and scopes single-instance ownership to
`REASONIX_HOME` instead of the executable path. Switching saved sessions is
now transactional: a target build, restore, or lease failure leaves the
current controller, lease, path, mode profile, and runtime epoch untouched.
- Stabilized the desktop rich composer caret after skill and plugin invocation
tags. DOM→model and model→DOM selection mapping now treat invocation chips as
zero-length atoms while still counting user text that lands inside the NBSP
caret anchor (common on Windows WebView2), restore both selection ends, and
recover the insertion point from a `beforeinput` snapshot when the browser
temporarily loses selection — so mid-text edits no longer jump to the end.
- Isolated the Windows desktop WebView2 shell from stale system proxies, so an
exited proxy client cannot leave the embedded UI hidden during startup. If
WebView2 still does not reach DOM-ready within 15 seconds, Reasonix now shows
the native window with a recovery prompt instead of appearing not to launch.
Remote Markdown images are fetched by the backend with Reasonix's proxy
configuration instead of bypassing that proxy through the isolated WebView.
- Restored captured-mouse right-click text paste, made composer drag selection
copy through the verified native clipboard path, and kept non-Git footer
telemetry left-aligned without reserving an empty data band.
- Restored stateful MCP behavior after the v1.17.13 regression: user-added
servers work without extra trust settings (including delivery-mode on-demand
calls), repository-provided servers use one exact launch confirmation, and
stdio tools reuse one persistent process so browser sessions survive across
calls without repeated startup latency. The former trust/reverify/catalog
management UI and CLI are removed.
- Localized persistent-footer labels and displayed work-mode values in English,
Simplified Chinese, and Traditional Chinese, while keeping command arguments
stable.
- Restored the `0.53` content boundary: model output, tool output, session
transcripts, recovery branches, and background-job artifacts retain their
original text instead of being rewritten by heuristic secret redaction.
Credential masking remains in key-entry summaries and explicit diagnostic or
session-cleanup paths. Transcript-bearing session/job sidecars are kept
private (`0600`, with private job directories), and the retired
`redact_tool_output` setting is removed with a one-time upgrade notice.
### Notes
- Full bilingual release notes:
<https://reasonix.io/changelog/v1.20.0/> ·
[GitHub release](https://github.com/esengine/DeepSeek-Reasonix/releases/tag/desktop-v1.20.0).
- The detailed entries below accumulated on `main-v2` after 1.0.0 and shipped
across 1.1.0–1.20.0; per-version attribution lives in the per-version release
notes linked above.
## 1.1.0 – 1.19.7
Per-version entries for the intermediate releases are published in the
[bilingual release notes](https://reasonix.io/changelog/) and on the
[GitHub releases page](https://github.com/esengine/DeepSeek-Reasonix/releases).
## [1.0.0] — 2026-06-03
First stable release — a **ground-up rewrite in Go**. Not an upgrade of the `0.x`
TypeScript line; a new codebase that becomes the default (`main-v2`).
### Highlights
- **Go kernel**: a single static binary (CGO-free), cross-compiled for
darwin/linux/windows on amd64 + arm64. Distributed via npm (the package wraps
the native binary), Homebrew (`esengine/reasonix` tap), and release archives;
no Node runtime needed to run it.
- **Agent core**: the loop, built-in tools (read/write/edit/multi_edit/glob/grep/
ls/bash/web_fetch/todo_write), permission gate, sandboxed bash, and the
DeepSeek prefix-cache–oriented design.
- **Subagents**: `task` plus explore/research/review/security_review skill agents.
- **Skills & hooks**: Claude-Code-style skills (`internal/skill`) and hooks
(`internal/hook`), symlink-aware and slash-integrated.
- **MCP client**: connect external servers over stdio / Streamable HTTP; reads
`[[plugins]]` and a Claude-Code `.mcp.json`.
- **Code intelligence via CodeGraph**: a tree-sitter symbol/call graph
(`codegraph_*` tools) replaces embedding semantic search — no embedding service
or API cost. Fetched into a local cache on first use (or `reasonix codegraph
install`) and indexed in the background, so installs and startup stay fast.
- **Plan mode** with evidence-backed step sign-off (`complete_step`).
- **Memory**: `REASONIX.md` hierarchy + auto-memory, folded into the cache-stable
prefix.
- **ACP** (`reasonix acp`) and an HTTP/SSE server frontend; desktop app (Wails).
### Fixed
- **File encoding support restored** — GBK/GB18030 (and other non-UTF-8) files
can now be read, edited, and grepped correctly. The v2 rewrite had dropped
v1's encoding detection; files in CJK Windows charsets were silently misread
or rejected as binary. The read/edit/write round-trip now preserves the
original file encoding. (#2637)
### Notes
- Versions: the legacy TypeScript line stays in `0.x`; the Go line starts at
`1.0.0`. See [docs/MIGRATING.md](docs/MIGRATING.md).
- Release archives ship a bare binary; CodeGraph is fetched on first use. Windows
support for the fetched runtime is unverified — install `codegraph` on PATH if
the auto-fetch doesn't resolve there.
[1.20.0]: https://github.com/esengine/DeepSeek-Reasonix/releases/tag/desktop-v1.20.0
[1.0.0]: https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.0.0
-274
View File
@@ -1,274 +0,0 @@
# ACP 编辑器接入
<a href="../README.zh-CN.md">README</a>
&nbsp;·&nbsp;
<a href="./ACP.md">English</a>
&nbsp;·&nbsp;
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="https://agentclientprotocol.com/">ACP 规范</a>
Reasonix 实现了 Agent Client Protocol(ACP)v1,通过标准输入输出提供 NDJSON
JSON-RPC 2.0 agent。编辑器和其他 ACP host 负责启动进程、打开一个或多个工作区会话,
并接收流式消息、工具活动、计划、权限请求和配置更新。
会话状态 usage 可携带结构化 `costQuote`(原币、`originalTotals`、identity/官方区域价表
估值、`costComplete`、`displayComplete`、`displayStatus`、`billingMode`),同时保留镜像所选
展示估值的旧字段 `estimatedCost` / `currency`。
详见 [计费文档](./BILLING.zh-CN.md)。
## 启动 agent
ACP host 应启动以下命令之一:
```sh
reasonix acp
reasonix acp --model deepseek-pro
reasonix acp --preset delivery
```
客户端未覆盖模型时,`--model` 用于选择启动模型;`--preset` 用于选择启动执行设定:
`light`、`balanced` 或 `delivery`(默认 `balanced`)。兼容的
`--profile economy|balanced|delivery` 仍可用(`economy` → `light`)。初始化后,
两者仍可按会话切换。
标准输出专用于 ACP 消息,Reasonix 会把诊断写入标准错误,因此 host 不应合并这两个
流。尚未配置 provider 时先运行 `reasonix setup`;initialize 响应也会声明一个启动
`reasonix setup` 的 terminal authentication method。
## 初始化与能力协商
客户端应在打开会话前调用 `initialize`。Reasonix 会声明以下能力结构(省略无关字段):
```json
{
"protocolVersion": 1,
"agentCapabilities": {
"loadSession": true,
"sessionCapabilities": {
"list": {},
"resume": {},
"close": {},
"delete": {}
},
"promptCapabilities": {
"image": false,
"audio": false,
"embeddedContext": true
},
"mcpCapabilities": {
"http": true,
"sse": false
},
"_meta": {
"reasonix.io": {
"sessionSteer": {
"method": "_reasonix.io/session/steer"
}
}
}
}
}
```
客户端声明 `fs.readTextFile`、`fs.writeTextFile` 或 `terminal` 后,Reasonix 会让
适用的文件操作经过编辑器的未保存 buffer,并让适用的前台命令在客户端持有的 terminal
中运行。读取、编辑、写入等全部文件工具都参与其中,因此一次编辑作用于编辑器当前显示
的内容,而不是磁盘上最后保存的副本。非 UTF-8 文件不适用:ACP 的文件方法只处理文本,
这类文件会留在本地的编码保持路径上,原有字符集不变。客户端没有声明这些能力时,常规
工作区工具会在 Reasonix 进程内本地运行。
## 会话生命周期
每个 ACP 会话都拥有独立的 Reasonix Controller、工作区根目录、模型、执行设定、协作
模式、审批模式、MCP 集合和持久化 transcript,会话之间不会泄漏状态。
| 方法 | 行为 |
| --- | --- |
| `session/new` | 为绝对路径 `cwd` 打开会话并返回配置状态。 |
| `session/load` | 打开持久化 ACP 会话,并通过 `session/update` 通知回放 transcript。 |
| `session/resume` | 打开持久化会话,但不回放 transcript。 |
| `session/prompt` | 执行一轮任务,流式发送更新,最后返回停止原因。 |
| `session/cancel` | 取消活动回合;它是一条 notification。 |
| `session/list` | 列出活动和持久化 ACP 会话,可按绝对路径 `cwd` 过滤。 |
| `session/close` | 停止活动会话并释放资源,但不删除历史。 |
| `session/delete` | 停止会话并删除其持久化 ACP 历史。 |
`session/new`、`session/load` 和 `session/resume` 可以携带 `mcpServers`。
Reasonix 支持 stdio、Streamable HTTP 和 legacy SSE server。
stdio `env` 和 HTTP `headers` 支持 ACP 官方的
`[{"name":"...","value":"..."}]` 结构,同时继续接受旧版 object-map 结构。
## 会话控制
Reasonix 把互不相关的选择拆成独立控制轴,而不是混在一个 mode selector 中:
| 控制项 | 可选值 | 协议入口 |
| --- | --- | --- |
| 协作模式 | `normal`、`plan`、`goal` | `modes` 和 `session/set_mode` |
| 模型 | 已配置的 `provider/model` | id 为 `model` 的 `configOptions` |
| 推理强度 | provider 支持的等级或 `auto` | id 为 `effort` 的 `configOptions` |
| 执行设定 | `light`、`balanced`、`delivery` | id 为 `agent_preset` 的 `configOptions`(兼容 id `work_mode`:`economy` → `light`) |
| 工具审批 | `ask`、`auto`、`yolo` | id 为 `tool_approval` 的 `configOptions` |
模型、推理强度、执行设定和工具审批统一使用 `session/set_config_option`。它的参数是
`sessionId`、`configId` 和 `value`,其中 `configId` 取 `configOptions` 中该选项的
`id`:
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "session/set_config_option",
"params": {
"sessionId": "session-id",
"configId": "tool_approval",
"value": "yolo"
}
}
```
注意字段名是 `configId`,不是 `optionId`。返回值是刷新后的完整 `configOptions`
数组;id 未知时返回 `-32602 InvalidParams`。
切换模型或推理强度时会重建会话 Controller,同时保留历史和其他控制轴;
执行设定(`agent_preset` / 兼容 `work_mode`)与工具审批只更新 gate,不重建 Controller。
旧客户端仍可使用 `session/set_model`。`session/set_mode` 也继续接受 legacy 值
`default` 和 `auto`,分别表示“常规 + 询问”和“常规 + Yolo”;新客户端应使用上面的
独立 selector。
## Prompt、更新与审批
`session/prompt` 支持文本 block 和内嵌文本 resource,不声明图片或音频能力。执行回合
期间,Reasonix 可能发送:
- agent 消息和思考内容 chunk;
- pending 和 completed 工具调用更新;
- 从 `todo_write` 生成的完整计划更新;
- 可用的斜杠命令;
- 当前 mode 和配置项更新;
- 针对受权限控制工具及用户问题的 `session/request_permission` 请求。
Host 应让 `session/prompt` 请求保持打开,直到 Reasonix 返回停止原因;期间仍需同时处理
双向 request 和 notification。
## 回合中引导扩展
Reasonix 通过 ACP v1 厂商扩展提供回合中引导。它不是 ACP 核心方法,也不是仍未发布的
ACP v2 `session/inject` 提案。
### 发现能力
从以下位置读取方法名:
```text
agentCapabilities._meta["reasonix.io"].sessionSteer.method
```
不要假设该扩展一定存在,也不要调用无命名空间的 `session/steer`。ACP 为核心协议保留
所有不以下划线开头的方法名。
### 发送引导
在 `session/prompt` 仍处于活动状态时调用声明的方法:
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "_reasonix.io/session/steer",
"params": {
"sessionId": "session-id",
"prompt": [
{"type": "text", "text": "把用户名改成邮箱"}
]
}
}
```
持久化会话会返回 item id 和 disposition:
```json
{"itemId":"inbox-item-id","disposition":"steer_accepted"}
```
Reasonix 会先完成持久化再返回。`steer_accepted` 表示活动回合已接受;
`queued_followup` 表示 admission 竞争失败或当前没有活动回合,同一条持久化消息会保留为
后续回合。无路径兼容会话可能不返回 `itemId`,但仍返回 `steer_accepted`。已应用的消息
会进入正常历史;回放 transcript 时显示用户原文,不显示内部 steer marker。
| 条件 | JSON-RPC 结果 |
| --- | --- |
| 活动 prompt 接受持久化引导 | `{"itemId":"...","disposition":"steer_accepted"}` |
| 引导已持久化但活动 admission 被拒绝 | `{"itemId":"...","disposition":"queued_followup"}` |
| session 不存在或 prompt 为空 | `-32602 InvalidParams` |
| 无路径兼容 session 没有活动 prompt | `-32600 InvalidRequest` |
| 客户端调用 `session/steer` | `-32601 MethodNotFound` |
收到 `InvalidRequest` 时,兼容会话没有把引导入队。
## 持久化 Session Inbox 扩展
从 `agentCapabilities._meta["reasonix.io"].sessionInbox` 发现带版本的队列。
Schema v1 在 `methods` map 中声明方法名;客户端应使用这里声明的名字,不要自行拼接
vendor method。
| Key | 用途 | 主要参数 |
| --- | --- | --- |
| `enqueue` | 持久化 follow-up 或 steer | `sessionId`、`text`,可选 `intent`、`idempotencyKey` |
| `list` | 读取元数据、容量、暂停和恢复状态 | `sessionId` |
| `get` | 按需读取一条完整 envelope | `sessionId`、`itemId` |
| `update` / `delete` | 编辑或删除待处理项 | `sessionId`、`itemId` |
| `move` | 调整待处理项顺序 | `sessionId`、`itemId`、从 0 开始的 `toIndex` |
| `setPaused` | 暂停或恢复派发 | `sessionId`、`paused` |
| `retry` / `refresh` | 重试不确定项或重新冻结引用 | `sessionId`、`itemId` |
`enqueue` 返回 `itemId`、`disposition`、`position`、`paused` 和 `idempotent`。
List 只返回预览和字节数,不返回正文。恢复出的 Inbox 默认暂停;客户端应先让用户检查,
再用 `setPaused: false` 恢复派发。
## 运行时重载与扩展表面
Reasonix 还在 `agentCapabilities._meta["reasonix.io"]` 中通告两个扩展点:
- `sessionReloadExtensions`——vendor method
`_reasonix.io/session/reloadExtensions`。调用后按与 CLI `/reload`
相同的失败原子语义重载该会话的 agent 运行时(扩展、工具、skills、
commands、hooks、providers):回合或重建进行中只排队一次
(`{"queued": true}`),空闲后执行;否则原子重建并交换,重建失败时
保留旧运行时。重载成功后 Reasonix 会推送新的
`available_commands_update`。
- `extensionSurface`——结构化扩展 UI 能力。在 initialize `_meta` 中
同样声明了 `reasonix.io.extensionSurface` 的客户端会收到结构化的
扩展表面载荷;未声明的客户端收到等价文本 fallback(card/status 退
化为 `agent_message_chunk`,扩展表单退化为权限请求),因此客户端
不做任何处理也能保持兼容。
已安装插件声明的扩展 action 以 `/<plugin>:<action>` 出现在
`available_commands_update` 中,可像普通斜杠命令一样调用。
## 兼容性与缓存行为
| 表面 | 旧版或非 Reasonix 客户端的行为 | 结论 |
| --- | --- | --- |
| 现有 ACP v1 方法 | 方法名和响应结构不变。 | 兼容 |
| Capability `_meta` | 可以忽略未知 metadata。 | 兼容 |
| 持久化 transcript | transcript schema 不变;Inbox 使用带版本的 sidecar。 | 兼容 |
| CLI、Desktop、Bot steer | 被拒绝的 steer 会保留为持久化 follow-up。 | 兼容 |
Steer 只会把用户请求的消息追加到正常会话历史,不改变 system prompt、工具 schema、工具
顺序或其他稳定的 provider prefix 字节。下一次 provider 请求必然包含这条新消息,和任何
普通新用户消息一样会改变新增后缀,但此前的稳定前缀仍可复用。
## 客户端接入检查清单
1. 启动 `reasonix acp`,分离 stdin、stdout 和 stderr。
2. 调用 `initialize`,同时遵守标准 capability 和 `_meta` capability。
3. 使用绝对工作区路径打开会话,并隔离保存各 session id。
4. Prompt 运行期间继续处理 agent 发往客户端的文件、terminal 和权限请求。
5. 只有在 Reasonix 声明 capability 且 prompt 活动时才显示 steer UI。
6. 按 steer `disposition` 分支;两种结果都已持久化,但只有 `steer_accepted`
能影响活动回合。
7. 用 `session/close` 释放资源;只有用户明确要删除持久化历史时才调用
`session/delete`。
-59
View File
@@ -1,59 +0,0 @@
# 计费、展示币种与费用报价
Reasonix 将三类事实分开:
1. `original`:按公开/自定义价表计算的原币估算,不是发票或实际扣款。
2. `valuations`:调用发生时记录的 `identity`,以及可用时同模型另一官方区域的
`official_table` 估算。
3. 钱包余额:供应商接口返回的原币事实。
运行时不再下载、缓存或刷新 FX,也不换算钱包。旧 JSON 中的 `fx`、
`rateSnapshot` 仍可读取为历史估值;新报价永远不会生成它们。
```toml
[billing]
display_currency = "auto" # auto | CNY | USD
[[providers]]
billing_currency = "USD" # 价表基准币种,不代表实际结算币种
billing_mode = "payg" # payg | subscription_equivalent
```
旧 `[desktop].currency` 仍可读并迁移到 `[billing].display_currency`。
`auto` 在配置层保持未解析:单一有效钱包币种可以成为当前 tab/session 的运行时
提示;否则单一原币直接展示,混币则按币种分桶。语言、浏览器 locale、主机区域都不再
改变价表。
## CostQuote
`usage.costQuote` 是所有主机表面的规范 usage 载荷:
| 字段 | 含义 |
| --- | --- |
| `original` | 原币价表估算 |
| `originalTotals[]` | 混币聚合时按 ISO 排序的原币桶 |
| `valuations.*.basis` | 新报价只有 `identity` 或 `official_table` |
| `selected` | 只有形成单一展示总额时才存在 |
| `costComplete` | usage 与价表事实完整 |
| `displayComplete` | 已形成请求的单币种展示总额 |
| `complete` | 兼容别名,始终镜像 `displayComplete` |
| `displayStatus` | `matched`、`fallback_original`、`bucketed`、`unavailable` |
| `aggregateMode` | `single_currency`、`common_valuation`、`currency_buckets` |
目标币种缺失但所有原币相同时,使用 `fallback_original` 展示原币;混币输出
`originalTotals`,不写伪造的 0。只有缺少 usage/价格时才是 `unavailable` 并显示 `—`。
旧标量别名(`cost`、`costUsd`、`total_cost`)仅在存在 `selected` 时双写。
## 钱包与诊断
钱包余额不换算、不跨币种求和。显式目标有对应钱包时显示该钱包;没有时显示真实币种
并加 ISO 前缀。自动模式只有一个有效钱包币种时才作为运行时费用提示;多钱包、未知币种
或请求失败不会影响费用事实。
```sh
reasonix doctor billing
reasonix doctor billing --json
```
兼容保留的 `fx` 对象固定为 `enabled=false`、无缓存;正文同时展示自动选择策略、价表
基准币种和官方目录匹配情况。
-443
View File
@@ -1,443 +0,0 @@
# Reasonix 机器人使用指南
<a href="../README.zh-CN.md">README</a>
&nbsp;·&nbsp;
<a href="./BOT_GUIDE.md">English</a>
&nbsp;·&nbsp;
<a href="./GUIDE.zh-CN.md">通用指南</a>
> 面向桌面端和 CLI 用户。本文说明如何连接飞书、Lark、微信和 QQ 机器人,
> 如何在 IM 里使用 Reasonix,以及审批、问答、YOLO 和常用命令的交互方式。
## 目录
- [能做什么](#能做什么)
- [在哪里运行](#在哪里运行)
- [连接四个渠道](#连接四个渠道)
- [无界面运行 Bot](#无界面运行-bot)
- [使用流程](#使用流程)
- [四种渠道的交互差异](#四种渠道的交互差异)
- [命令速查](#命令速查)
- [审批与 YOLO](#审批与-yolo)
- [升级后是否需要重新绑定](#升级后是否需要重新绑定)
- [排障](#排障)
## 能做什么
连接机器人后,你可以在飞书、Lark、微信或 QQ 里给 Reasonix 发消息,让桌面端
Reasonix 或 `reasonix bot start` 进程在本机执行同一套模型、工具、权限与
沙盒逻辑。
典型场景:
- 让 Reasonix 查代码、读文档、解释错误、整理结论。
- 在 IM 中触发工具调用,并把执行过程和结果回传到聊天窗口。
- 遇到写文件、执行命令等敏感操作时,在 IM 中审批或拒绝。
- 对临时测试任务开启 YOLO,跳过普通工具审批。
- 打开桌面端对应 IM 会话,继续查看上下文、成本、tokens 和工具轨迹。
## 在哪里运行
Bot gateway 是一套共享的 Go runtime。核心行为在 Windows、macOS 和 Linux
上都生效;实际差异主要来自各 IM 平台的凭据、网络、回调或 WebSocket 配置,
以及本机保存的账号状态。
目前有两个入口:
- **桌面端 runtime**:在 **设置 -> 机器人** 中配置。桌面端会启动 gateway,
在应用内维护状态,持久化每个连接的工具审批模式变化,并允许打开匹配的
本地 IM 会话。
- **CLI runtime**:执行 `reasonix bot start` 启动无界面长期进程。它复用
与桌面端相同的配置、白名单、路由、队列设置、配对存储、适配器和
项目/会话索引。
普通 `reasonix run` 不会自动启动 IM 网关。只有桌面端 bot runtime 正在运行,
或存在一个存活的 `reasonix bot start` 进程时,远端 IM bot 能力才会生效。
## 连接四个渠道
打开桌面端 Reasonix,进入 **设置 -> 机器人**。在 **添加 IM Bot** 区域选择
渠道并扫码。
```mermaid
flowchart LR
A["打开桌面端设置"] --> B["机器人"]
B --> C["添加 IM Bot"]
C --> D{"选择渠道"}
D --> E["飞书扫码创建 PersonalAgent"]
D --> F["Lark 扫码创建 PersonalAgent"]
D --> G["微信扫码登录 Bot 助手"]
D --> H["QQ 手动配置 Bot 助手"]
E --> I["连接保存到本机"]
F --> I
G --> I
H --> I
I --> J["在 IM 中发送第一条消息"]
J --> K["桌面端创建对应会话"]
```
### 飞书
1. 在 **设置 -> 机器人 -> 添加 IM Bot** 里选择 **飞书**。
2. 点击生成二维码。
3. 用飞书扫码并完成授权。
4. 等待页面显示已连接。
5. 给飞书 Bot 发送消息,例如 `你好` 或 `帮我看一下这个报错`。
### Lark
1. 在 **设置 -> 机器人 -> 添加 IM Bot** 里选择 **Lark**。
2. 点击生成二维码。
3. 用 Lark 扫码并完成授权。
4. 等待页面显示已连接。
5. 给 Lark Bot 发送消息。
飞书和 Lark 使用同一套能力,但作为两个独立连接保存。你可以给它们设置不同
模型、工作目录或工具审批模式。Bot 文本回复会以独立 Interactive Card JSON
2.0 markdown 发送,避免飞书/Lark 平台级引用前缀,同时保留 CommonMark
格式;如果卡片超过平台限制,Reasonix 会自动降级为纯文本。
Webhook 模式需要配置 verification token。传入事件会 fail-closed 校验:如果
配置中的 token 为空或缺失,调用方会被拒绝,不会静默开放 webhook。
### 微信
1. 在 **设置 -> 机器人 -> 添加 IM Bot** 里选择 **微信**。
2. 点击生成二维码。
3. 用微信扫码登录 Bot 助手。
4. 等待页面显示已连接。
5. 给微信 Bot 发送消息。
微信没有交互卡片按钮,因此审批通过数字或文字命令完成。Ask 问题可以直接
回复普通文本、选项编号,或使用 `/answer <id> <回答>`。
### QQ
1. 在 **设置 -> 机器人 -> 添加 IM Bot** 里选择 **QQ**。
2. 填写 **App ID** 和 **App Secret**(或设置环境变量 `QQ_BOT_APP_SECRET`)。
3. 点击 **保存** 存储凭据。
4. 等待页面显示已连接。
5. 给 QQ Bot 发送消息。
QQ Bot 使用官方 QQ Bot 平台 API。它支持内联键盘按钮来完成审批。
Ask 问答会以文字形式发送;可以直接回复普通文本、选项编号,或使用
`/answer <id> <回答>`。当按钮过期或平台提示操作失败时,可以直接复制
卡片里的 ID,用文字命令继续。
QQ 不支持扫码连接,必须手动配置 App ID 和 App Secret。适配器只读取配置的
`app_secret_env`,不会再回退读取无关的 `QQ_SECRET` 环境变量。QQ 和微信的
HTTP 调用使用带超时的 client,避免平台请求卡住后无限阻塞 gateway。
## 无界面运行 Bot
桌面端是创建和测试 Bot 连接最简单的入口,但 Bot 运行时也可以作为长期运行的
无界面网关启动:
```sh
reasonix bot doctor
reasonix bot doctor --deep
reasonix bot start --channels qq,feishu,lark,weixin --dir /path/to/project
```
`--channels` 用来选择接受哪些已配置的 IM 输入。`feishu` 和 `lark` 会选择对应
飞书系连接,`weixin` 会选择已保存的微信 iLink 账号,`qq` 会选择已配置的 QQ
Bot。`--dir` 用来把远端消息绑定到某个项目工作区,`--model` 可以为这个进程
临时覆盖默认模型。
无界面网关复用桌面端保存的同一套配置:
- `[[bot.connections]]` 标识每个 IM 输入。`provider` 是适配器类型
(`feishu`、`weixin` 或 `qq`),`domain` 用来区分飞书和 Lark 等变体。
- `credential.app_id`、`credential.app_secret_env`、`credential.account_id`
和 `credential.token_env` 指向应用 ID、应用密钥、保存的账号或 token。
密钥仍保存在环境变量或 Reasonix 用户凭据中。
- `workspace_root`、`model` 和 `tool_approval_mode` 可以按连接单独设置,
因此不同 IM 渠道可以路由到不同本地项目或审批模式。
- `access` 也可以按连接单独设置,包括 `enabled`、`allow_all`、
`pairing_enabled`、`users`、`groups`、`admins` 和 `approvers`。当某个连接
有启用中的 access 配置时,会优先检查该连接自己的访问控制,再退回旧的全局
`[bot.allowlist]`。
- `[[bot.routes]]` 可以继续按远端连接、平台、会话类型、chat ID、用户 ID
或 thread ID 做更细粒度路由;空匹配字段表示通配,按配置顺序第一个命中。
命中后可覆盖 `workspace_root`、`model` 和 `tool_approval_mode`。
- `session_mappings` 会根据收到的远端消息自动填充远端 chat ID 和作用域。
只有当该映射同时具备本地 `session_id` 目标时,桌面端才能打开对应会话;
例如桌面端托管的 Bot runtime 保存了 `path:` 会话目标,或用户手动配置了
映射目标。
- Bot 的项目/会话索引只来自已配置的 `workspace_root`、route workspace、
当前活跃 bot 会话,以及已保存的 `session_mappings`。`/use project` 和
`/attach session` 只能跳到这些已索引目标;IM 文本里临时输入的任意本地
目录不会被接受。
访问控制仍然是必需项。桌面端新建的 Bot 通常建议在该 Bot 自己的详情面板里
设置谁可以使用,这会保存到 `[[bot.connections]]` 或 `[bot.qq].access`。
旧的全局 `[bot.allowlist]` 仍然作为兼容兜底,适用于旧配置或没有启用单
Bot access 的连接。你可以有意设置 `allow_all = true`,也可以为单个 Bot
启用 `pairing_enabled`,或全局启用 `[bot.pairing]`,让未知私聊用户先收到
一次性配对码。配对码需要在本机执行 `reasonix bot pairing approve <code>`
后才会放行;如果该请求带有连接 ID,批准后会把发送者加入对应 Bot 自己的
access 名单。列在 `admins` / `approvers` 或旧的 `*_admins` /
`*_approvers` 里的用户也会获得基础 bot 准入,不需要再重复写进 `users` /
`*_users`。群聊不会因为私聊配对或角色准入自动开放,群 ID 仍是额外收窄
条件。常用配对管理命令:
```sh
reasonix bot pairing list
reasonix bot pairing approve CODE
reasonix bot pairing reject CODE
```
如果配置了 `qq_admins`、`feishu_admins`、`weixin_admins` 或对应
`*_approvers`,`/yolo`、`/mode` 等运行模式命令只允许 admin 使用;
`/projects`、`/use project`、`/sessions`、`/attach session` 和
`/search all` 也只允许 admin 使用。`/approve` 和 `/deny` 只允许 approver
或 admin 使用。没有配置角色列表时,为兼容旧配置,已允许的用户保持原有
命令能力。远端用户进入的是同一个 Reasonix controller、权限策略、工具
审批模式和沙盒边界,和本地桌面端或 CLI 回合一致。
```toml
[bot.allowlist]
enabled = true
feishu_users = ["ou_member"]
feishu_admins = ["ou_admin"]
feishu_approvers = ["ou_approver"]
```
默认启用 `ignore_self_messages = true`。网关会记录刚发出的平台
`message_id`,并忽略平台回传的同 ID 消息;如果某个平台不会稳定回传同一
消息 ID,可以在 `[bot.self_user_ids]` 里配置 bot 自己的用户 ID 作为第二层
回声防护。`/status` 会显示当前会话队列模式和各连接的健康状态,例如
`feishu-lark=running` 或 `weixin-weixin=degraded`。
可选的 `[bot.control]` 提供本机 loopback HTTP API,默认关闭。启用后必须
设置 `token_env` 对应的环境变量,所有请求都需要
`Authorization: Bearer <token>`;地址只能绑定 `localhost`、`127.0.0.1`
或 `::1`。当前接口包括 `GET /status`(会话与连接健康快照)和
`GET /metrics`(Prometheus 文本指标)以及 `POST /send`(向指定连接发送
文本或媒体消息)。
示例:
```sh
export REASONIX_BOT_CONTROL_TOKEN="change-me"
curl -H "Authorization: Bearer $REASONIX_BOT_CONTROL_TOKEN" \
http://127.0.0.1:37913/status
curl -X POST http://127.0.0.1:37913/send \
-H "Authorization: Bearer $REASONIX_BOT_CONTROL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "feishu-lark",
"domain": "lark",
"chat_id": "oc_xxx",
"chat_type": "dm",
"text": "hello from local control API"
}'
```
## 使用流程
```mermaid
sequenceDiagram
participant U as "用户"
participant IM as "飞书 / Lark / 微信 / QQ"
participant R as "Reasonix 桌面端或 bot start"
participant T as "本机工具与模型"
U->>IM: "发送需求"
IM->>R: "消息进入本机 Bot 网关"
R->>T: "模型思考并调用工具"
alt "普通回复"
R-->>IM: "返回答案"
else "需要审批"
R-->>IM: "发送审批卡片或审批文本"
U->>IM: "允许 / 拒绝"
IM->>R: "审批命令"
R->>T: "继续或停止工具调用"
R-->>IM: "返回结果"
else "需要用户选择"
R-->>IM: "发送 Ask 问题"
U->>IM: "选择选项或回复 /answer"
R-->>IM: "继续执行并返回结果"
end
```
桌面端左侧的 **机器人** 入口会显示已连接 Bot。收到第一条 IM 消息后,可以
从这里打开对应本地会话,查看上下文、工具轨迹、成本和运行指标。
## 四种渠道的交互差异
下面三张图是虚构内容的交互示意,用来帮助理解真实软件里的操作形态。
![飞书审批卡片示意](./assets/bot-feishu-approval.svg)
![Lark 开启 YOLO 示意](./assets/bot-lark-yolo.svg)
![微信文字命令示意](./assets/bot-weixin-text-commands.svg)
![QQ 审批卡片示意](./assets/bot-qq-approval.svg)
| 渠道 | 连接方式 | 审批方式 | Ask 问答 | 适合场景 |
| --- | --- | --- | --- | --- |
| 飞书 | 扫码创建 PersonalAgent | 交互卡片按钮,也可用命令 | 交互卡片按钮,也可用命令 | 国内飞书工作流、群聊或个人助手 |
| Lark | 扫码创建 PersonalAgent | 交互卡片按钮,也可用命令 | 交互卡片按钮,也可用命令 | 国际版 Lark 工作流 |
| 微信 | 微信扫码登录 | 回复 `1` / `2` 或命令 | 回复普通文本、选项编号或命令 | 微信个人测试、轻量移动触发 |
| QQ | 手动配置(App ID + App Secret) | 内联键盘按钮、数字回复或命令 | 回复普通文本、选项编号或命令 | QQ 群聊、个人会话和官方 QQ Bot 平台 |
飞书和 Lark 的卡片按钮会在后台转换为命令,例如 `/approve <id>`、
`/deny <id>` 或 `/answer <id> <选项>`。QQ 的审批按钮也是如此。
如果按钮过期或平台提示操作失败,可以直接复制卡片里的 ID,用文字
命令继续。
## 命令速查
这些命令在飞书、Lark、微信和 QQ 中通用。
| 命令 | 作用 | 示例 |
| --- | --- | --- |
| `/help` | 查看可用命令 | `/help` |
| `/status` | 查看活跃任务、队列、工具审批模式和连接健康 | `/status` |
| `/stop` | 停止当前任务 | `/stop` |
| `/new` | 开始新会话 | `/new` |
| `/reset` | 重置当前会话 | `/reset` |
| `/approve <id>` | 批准待审批操作 | `/approve 1` |
| `/deny <id>` | 拒绝待审批操作 | `/deny 1` |
| `/answer <id> <选项>` | 回答 Ask 问题 | `/answer ask-1 2` |
| `/yolo` | 开启 YOLO | `/yolo` |
| `/yolo on` | 开启 YOLO | `/yolo on` |
| `/yolo off` | 切回询问模式 | `/yolo off` |
| `/yolo auto` | 切换到自动审批模式 | `/yolo auto` |
| `/yolo status` | 查看当前工具审批模式 | `/yolo status` |
| `/mode yolo` | 切换到 YOLO | `/mode yolo` |
| `/mode ask` | 切换到询问模式 | `/mode ask` |
| `/mode auto` | 切换到自动模式 | `/mode auto` |
| `/queue status` | 查看当前队列模式 | `/queue status` |
| `/queue steer` | 运行中消息作为当前任务补充 | `/queue steer` |
| `/queue followup` | 运行中消息排队为后续回合 | `/queue followup` |
| `/queue collect` | 合并排队消息为一个后续回合 | `/queue collect` |
| `/queue interrupt` | 取消当前任务并处理最新消息 | `/queue interrupt` |
| `/queue list` / `/queue show <n\|id>` | 查看持久化排队内容 | `/queue list` |
| `/queue delete <n\|id>` / `/queue move <n\|id> <位置>` | 删除或调整待处理项顺序 | `/queue move 2 1` |
| `/queue pause` / `/queue resume` | 暂停或恢复自动派发 | `/queue resume` |
| `/queue retry <n\|id>` / `/queue refresh <n\|id>` | 重试不确定项或刷新冻结引用 | `/queue retry 1` |
| `/projects [关键词]` | 查看已索引项目工作区 | `/projects reasonix` |
| `/use project <id\|名称>` | 将当前远端会话路由到已索引项目 | `/use project p1` |
| `/use project default` | 清除项目覆盖,恢复配置路由 | `/use project default` |
| `/sessions search <关键词>` | 搜索已索引桌面/bot 会话 | `/sessions search 发布 bug` |
| `/attach session <id\|关键词>` | 从已索引 `path:` transcript 继续当前远端会话 | `/attach session s1` |
| `/search all <关键词>` | 跨已索引项目检索文件内容 | `/search all TODO` |
快捷回复:
- 有待审批操作时,回复 `1` 表示批准,回复 `2` 表示拒绝。
- 有待处理 Ask 问题时,可以直接回复任意普通非 slash 文本;选择题仍然可以
直接回复选项编号。
- `/stop`、`/mode`、`/answer ...` 等 slash 命令不会被 Ask 快捷回复截获。
- 如果没有待处理操作,`1` / `2` 会被当作普通消息或收到提示。
默认队列模式是 `steer`:同一会话正在运行时,新消息会先持久化,再作为当前
任务的 mid-turn guidance 注入;当前回合无法接收时,同一条消息会保留为后续
回合。容量采用失败关闭:每个会话最多 64 条、单条 4 MiB、合计 64 MiB,
`queue_drop` 不再删除旧消息。崩溃恢复后 Inbox 默认暂停,需检查后执行
`/queue resume`。`reasonix bot doctor --deep` 会显示队列、配对和角色诊断信息。
队列模式:
- `steer`:运行中的消息会尽量作为当前回合的补充指导。
- `followup`:运行中的消息排队成为后续回合。
- `collect`:把排队消息合并成一个后续回合。
- `interrupt`:取消当前回合,并保留最新消息作为下一回合。
项目与会话跳转:
- `/projects [关键词]` 会列出来自 bot route、连接工作区、活跃 bot 会话和
已保存 session mapping 的工作区。
- `/use project <id|名称>` 会把当前远端会话固定到某个已索引项目;
`/use project default` 会清除覆盖。
- `/sessions search <关键词>` 搜索已索引桌面端和 bot 会话元数据。
- `/attach session <id|关键词>` 从已索引 `path:` transcript 继续当前远端
会话。
- `/search all <关键词>` 跨已索引项目根目录检索文件内容;有 `rg` 时优先用
`rg`,否则使用带边界的 Go fallback scanner。
这些跳转命令不会接受 IM 中临时输入的任意本地路径,只能跳到索引内目标;
配置了角色列表时,这些命令需要 admin 权限。
当适配器提供媒体 URL 时,gateway 会把文件下载到当前工作区的
`.reasonix/attachments`,并以 `@.reasonix/attachments/...` 形式传给
Reasonix。保存失败的附件会在 IM 中提示,文本内容仍会继续处理。内置
Feishu、Weixin、QQ 适配器当前仍以文本事件为主,普通 IM 附件抽取可以继续在
适配器层补齐。
## 审批与 YOLO
Reasonix 的机器人沿用桌面端权限系统。默认是询问模式:写文件、执行命令等
敏感工具调用会先请求确认。
```mermaid
flowchart TD
A["模型准备调用工具"] --> B{"是否命中 deny 规则"}
B -- "是" --> C["直接拒绝"]
B -- "否" --> D{"工具审批模式"}
D -- "询问 Ask" --> E["向 IM 发送审批"]
D -- "自动 Auto" --> F["策略允许时自动放行"]
D -- "YOLO" --> G["普通工具审批自动放行"]
E --> H{"用户选择"}
H -- "允许" --> I["执行工具"]
H -- "拒绝" --> J["停止该操作"]
F --> I
G --> I
```
YOLO 的边界很重要:
- YOLO 会跳过普通工具审批。
- YOLO 不会跳过硬性 `deny` 规则。
- YOLO 不会自动回答模型提出的 Ask 问题。
- YOLO 不会自动批准计划模式里的计划批准。
建议:
- 临时调试、可信项目、需要快速连续读写时,可以用 `/yolo`。
- 做高风险操作、生产代码或不确定任务时,用 `/mode ask` 切回询问模式。
- 想减少普通审批但保留策略判断时,用 `/mode auto`。
## 升级后是否需要重新绑定
正常升级或覆盖安装 Reasonix app 后,不需要重新绑定。
绑定信息保存在用户配置目录,而不是 app 包内:
- Bot 连接、远端 ID、白名单、模型和审批模式保存在用户配置文件。
- 飞书和 Lark 的密钥保存在 CLI 与桌面端共用的 Reasonix 全局
`<Reasonix home>/.env`。
- 微信扫码后的账号 token 保存在 Reasonix 的用户数据目录。
- QQ 的 App ID 保存在用户配置文件;App Secret 通过配置的环境变量
(默认 `QQ_BOT_APP_SECRET`)保存在 Reasonix 全局凭据文件中。
需要重新扫码或重新配置的情况:
- 删除了 Reasonix 用户配置目录。
- 换了 macOS 用户或换了机器。
- 平台侧撤销授权。
- 微信 token 失效。
- 飞书或 Lark 应用密钥被清除。
- QQ 的 App ID 或 App Secret 失效或被更改。
## 排障
| 现象 | 可以检查 |
| --- | --- |
| 扫码提示链接失效 | 回到设置页重新生成二维码;二维码有有效期(飞书、Lark、微信;QQ 不使用扫码,请检查手动配置)。 |
| 已连接但没有回复 | 确认桌面端 bot runtime 或 `reasonix bot start` 进程正在运行,Bot 连接已开启,用户 ID 在白名单内、已配对或允许所有人。 |
| 飞书或 Lark 按钮提示失败 | 直接发送卡片里的命令,例如 `/approve <id>` 或 `/deny <id>`。 |
| QQ 按钮提示失败 | 与飞书/Lark 相同 —— 直接发送卡片里的命令,例如 `/approve <id>` 或 `/deny <id>`。 |
| 微信回复 `1` 没反应 | 只有存在待审批或 Ask 时数字快捷回复才生效;也可以使用完整命令。 |
| QQ 回复 `1` 没反应 | 与微信相同 —— 只有存在待审批或 Ask 时数字快捷回复才生效;也可以使用完整命令。 |
| 想确认当前模式 | 发送 `/status` 或 `/yolo status`。 |
| 想重新开始上下文 | 发送 `/new` 或 `/reset`。 |
| 想停止当前任务 | 发送 `/stop`。 |
如果仍然无法连通,可以在 **设置 -> 机器人** 中打开对应 Bot 的高级设置,使用
检查配置、测试发送和运行设置来定位问题。
-229
View File
@@ -1,229 +0,0 @@
# 能力诊断
<a href="./CAPABILITY_DIAGNOSTICS.md">English</a>
&nbsp;·&nbsp;
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="./PLUGIN_PACKAGES.zh-CN.md">插件包</a>
Reasonix 提供 CLI 与桌面端 **设置 → 诊断** 共用的只读能力诊断模型,覆盖 Skills、
Commands、Hooks、插件包、MCP 服务器,以及指令文件(`AGENTS.md` /
`REASONIX.md` / `CLAUDE.md`)。
**写入策略**
| 模式 | 配置文件 | MCP stats / schema cache | 网络 / MCP 进程 |
| --- | --- | --- | --- |
| 静态(默认)+ 桌面端 | 永不写入(`LoadForRootReadOnly`) | 永不写入 | 无 |
| CLI `--live` | 永不写入 | **不写入**(`SkipPersistence`) | 在隔离 Host 中启动 automatic MCP |
## 怎么用(快速上手)
| 目标 | 命令 / 入口 |
| --- | --- |
| 检查当前工作区的 skills / hooks / MCP / 插件 | `reasonix doctor capabilities` |
| 机器可读报告(CI / 报障) | `reasonix doctor capabilities --json` |
| 指定项目根目录 | `reasonix doctor capabilities --root /path/to/project` |
| 真实探测 MCP 启动(会启动第三方服务器) | `reasonix doctor capabilities --live --timeout 5s` |
| 让 Agent 按手册排障 | 会话中 `/reasonix-guide`,或自然语言描述症状 |
| GUI 健康视图 | 桌面端 **设置 → 诊断** |
**默认是静态且安全的**:无网络、不启动 MCP 子进程。只有你明确需要启动
automatic MCP 时才用 `--live`。
其它既有 doctor 命令(行为不变):
```bash
reasonix doctor # 环境 / provider / 沙箱快照
reasonix doctor session <id> # 支持用会话包
reasonix doctor redact-sessions # 脱敏会话中的密钥
```
## 日常工作流
### 1. 「Skill / 命令找不到或内容不对」
```bash
reasonix doctor capabilities --json | jq '.skills.entries, .commands.entries, .issues'
```
关注:
- `skill.shadowed` / `command.shadowed` — 更高优先级路径覆盖了它
- `skill.disabled` — 名字在 `[skills].disabled_skills` 里
- `skill.missing_description` — 能加载但索引描述很弱
- `command.read_failed` — 文件读失败或解析失败
然后到 **设置 → 技能**,或直接改 `.reasonix/skills` / `.reasonix/commands` 下的文件。
### 2. 「项目 Hooks 不触发」
```bash
reasonix doctor capabilities | sed -n '/Hooks/,/Plugins/p'
```
项目 Hooks 会从 `.reasonix/settings.json` 自动加载。若没有触发,请确认当前工作区,
保存后重启 Reasonix。`match` 是**锚定**正则:`file` **不会**匹配 `read_file`。
### 3. 「配置了 MCP 但模型看不到工具」
1. 先做静态检查(无副作用):
```bash
reasonix doctor capabilities --json | jq '.mcp.servers, .issues[] | select(.subsystem=="mcp")'
```
2. 仅在接受启动第三方服务器时:
```bash
reasonix doctor capabilities --live --timeout 10s --json
```
常见 code:`mcp.command_not_found`、`mcp.invalid_transport`、
`mcp.start_failed`、`mcp.no_tools`。桌面端更推荐 **设置 → 诊断** 打开
「包含当前会话运行状态」——只读取**活动标签 Host**,不会再起第二个 Host。
每个 MCP 条目通过 `source`、`source_path` 和 `effective` 标明真正生效的配置及其来源。
启动失败还会报告 `startup_stage`(`launch`、`authorization`、`initialize` 或
`tools/list`)、`startup_elapsed_ms`,以及有长度上限且已做凭据脱敏的 `stderr` 尾部。
这可以区分重复/被覆盖的注册与真正缓慢或失败的握手,同时不会暴露完整进程输出。
### 4. 让 Agent 按手册排查(`reasonix-guide`)
交互式会话中:
```text
/reasonix-guide
```
或:
```text
我配置了 MCP 服务器 X,但模型始终看不到它的工具,请排查。
```
该内置 Skill 是 **inline**(`runAs: inline`)。它会优先要求模型运行:
```bash
reasonix doctor capabilities --json
```
只有你明确允许启动外部 MCP 时才建议 `--live`。项目或全局同名
`reasonix-guide` 会覆盖内置版;也可用
`[skills].disabled_skills = ["reasonix-guide"]` 隐藏。
## CLI 参考
```bash
reasonix doctor capabilities [--root PATH] [--json] [--live] [--timeout 5s]
```
| 参数 | 含义 |
| --- | --- |
| `--root` | 工作区根目录(默认当前目录),走 `config.LoadForRoot` |
| `--json` | 仅向 **stdout** 输出一个 JSON 对象(提示写 stderr) |
| `--live` | 在隔离 Host 中启动 **automatic** MCP(可能联网) |
| `--timeout` | 单服务器 live 超时,**1s–60s**,默认 `5s`,必须配合 `--live` |
### 模式
| 模式 | 行为 |
| --- | --- |
| **静态(默认)** | 无网络;不启动 stdio / HTTP / SSE MCP 子进程 |
| **Live(`--live`)** | stderr 风险提示;只探测 automatic 启动意图;`auto_start=false` → `skipped`;并发 4;始终关闭 Host |
桌面端「包含当前会话运行状态」**不等于** CLI `--live`:桌面只**读取**活动标签 Host,
不启动 MCP。
### 退出码
| 码 | 含义 |
| --- | --- |
| `0` | 无 `error` 级问题(warning/info 允许) |
| `1` | 存在 `error` 或 live MCP 启动失败 |
| `2` | 参数错误 |
示例:
```bash
# 当前目录、人类可读
reasonix doctor capabilities
# CI:仅有 error 时非零退出
reasonix doctor capabilities --json
# live 探测,超时 15 秒
reasonix doctor capabilities --live --timeout 15s --json 2>live-warn.txt
```
既有 `reasonix doctor` / `doctor session` / `doctor redact-sessions` 的 JSON
schema **不会**混入新字段。
## 桌面端
打开 **设置 → 诊断**:
| 控件 | 行为 |
| --- | --- |
| 打开页面 | 对活动工作区根加载**静态**报告 |
| 刷新 | 按当前「会话运行状态」开关重新收集 |
| 复制脱敏 JSON | 可安全粘贴的报告(路径已脱敏) |
| 包含当前会话运行状态 | 仅合并活动标签 Host 的 connected / failed / deferred / disabled |
| 前往设置(Issue 上) | 当 `settings_tab` 有值时跳到 MCP / Skills / Plugins / Hooks |
页面不提供自动编辑、执行 hooks、自动启用或自动重连。打开诊断页**不会**
rebuild controller,也不会 snapshot 会话。
## JSON schema(version 1)
顶层字段:`schema_version`、`root`、`live`、`summary`、
`instructions` / `skills` / `commands` / `hooks` / `plugins` / `mcp`、`issues`。
插件包条目对 Manifest v2 是增量扩展:声明了代码型 Runtime 的插件还会
报告 `prompts` 与 `themes` 计数和 `runtime` 标记(见
<a href="./PLUGIN_PACKAGES.zh-CN.md">插件包</a>)。旧读者可以忽略这些
字段;`schema_version` 保持 `1`。
Issue 含稳定 `code`、`severity`、`subsystem`、`source`、`message`、`remediation`、
可选 `settings_tab`。数组与 Issue 顺序确定,便于脚本与测试。
常见 code:
- `skill.shadowed`、`skill.missing_description`、`skill.disabled`
- `command.shadowed`、`command.read_failed`
- `hook.invalid_matcher`、`hook.missing_command`、`hook.malformed_settings`
- `plugin.missing_root`、`plugin.invalid_manifest`、`plugin.compatibility`
- `mcp.invalid_transport`、`mcp.command_not_found`、`mcp.missing_command`、`mcp.missing_url`
- `mcp.start_failed`、`mcp.no_tools`、`mcp.runtime_unavailable`
### 严重度
| 严重度 | 含义 | CLI |
| --- | --- | --- |
| `error` | 配置损坏或 live 启动失败 | 退出 `1` |
| `warning` | 需处理但非致命 | 退出 `0` |
| `info` | 遮蔽、禁用、无运行时等 | 退出 `0` |
## 路径与密钥安全
路径显示为 `<workspace>/...`、`~/...` 或 `<external>/basename`。
不输出用户名、完整外部路径、环境变量值、Header 值、token、URL query。
MCP 仅列出 env/header 的 **key**。可能携带 HTTP 响应体或 MCP stderr 的
错误文本会先经过全局密钥脱敏器(Authorization、Bearer/JWT/厂商 token、
`KEY=value` 与 JSON `"key":"value"` 凭据形态、Cookie/Set-Cookie 值),
再截断到 400 字符。向 issue / 聊天贴报告时,优先复制诊断 JSON,
不要贴原始配置文件。
## 不在本诊断范围内的事项
| 需求 | 改用 |
| --- | --- |
| Provider 密钥、代理、沙箱 OS 支持 | `reasonix doctor` |
| 给支持用的完整会话包 | `reasonix doctor session <id>` |
| 单个插件包 | `reasonix plugin doctor <name>` |
| 会话内 MCP 列表 | `/mcp` |
## 缓存影响
内置 `reasonix-guide` 仅在 system prompt 的 Skill 索引中增加 **一行稳定索引**;
正文按需加载。诊断本身不进入 provider 请求。
-98
View File
@@ -1,98 +0,0 @@
# 设计:Checkpoints 与 Rewind
<a href="./CHECKPOINTS.md">English</a>
状态:**Phase 1 + 2 已实现**——包括快照存储、统一捕获切入点、Esc-Esc / `/rewind` CLI 选择器、桌面端悬停 rewind,以及完整的 Claude Code 风格菜单:恢复代码、恢复会话、同时恢复、从此处分叉、从此处开始摘要或摘要到此处。当前实现基于快照并与 Claude Code 对齐;可选的 git-backed 模式是优先级较低的后续工作。这补上了 v1 用户最常请求的编辑安全网 / 撤销能力。
本文说明 rewind 快照机制。关于自主运行期间智能体何时应暂停并询问用户,参见[任务合约与暂停策略](./TASK_CONTRACT.zh-CN.md)。
## 目标
让用户把会话回退到之前的节点,并恢复**代码**、**会话**或**两者**,且不改动 git 历史。CLI 与桌面端采用同一套机制,并与 Claude Code 的 Esc-Esc / `/rewind` 行为对齐。
## 机制:文件快照,而不是 git
与 Claude Code(以及 v1 的 `checkpoints.ts`)相同,checkpoint 是独立于 git 的**文件快照**:
- **不污染 git**:不提交、不暂存,也不修改 `.git/`;在非 git 目录中同样可用。
- **只跟踪可预览的编辑工具变更**:包括 `write_file`、`edit_file` 和 `multi_edit`。`move_file` 遵循同一工作区权限边界,但移动操作尚不会出现在 checkpoint 预览中。
- **不跟踪 `bash` 副作用**:系统无法判断 shell 命令改动了哪些内容,这一点与 Claude Code 相同;高风险 Bash 操作由权限层负责拦截。
- 编辑前保存完整文件内容。实现简单,存储量通过下文的保留策略限制。
可选的 **git-backed 模式**(v1 的 `auto-git-rollback`)适合需要 git 级安全保障的用户,但不在当前范围内。
## 锚点与捕获
- **每个用户回合创建一个 checkpoint。** 回合开始时(`Controller.Send` / `runTurn`)创建,并以用户提示作为标签。
- **编辑前快照。** 在 `agent.(*Agent).executeOne` 中,执行非只读且实现 `tool.Previewer` 的工具前,调用 `Preview(args)` 获得 `diff.Change{Path, Kind, OldText}`,再把文件快照记录到当前 checkpoint。文件写工具已经实现 `tool.Previewer`,因此只需这一处统一切入点,不必逐个修改工具。
- 同一回合内按路径去重:只保存**第一次**触碰前的内容,即该文件在回合开始时的状态。
- `Kind == create` 表示文件原本不存在,保存 `Content = nil`,恢复时删除该文件;`modify` / `delete` 保存 `OldText`。
- `bash` 没有实现 `Previewer`,因此自然排除,符合“只跟踪编辑工具”的约定。
## 数据模型
```go
type FileSnap struct {
Path string // workspace-relative
Content *string // nil → file did not exist at the anchor (restore deletes it)
}
type Checkpoint struct {
Turn int // user-message index this anchors (0-based)
Time time.Time
Prompt string // user message text — the picker label
Files []FileSnap // distinct files touched during this turn, turn-start state
}
```
## 存储
- **作为会话 sidecar 保存**:位于 `config.SessionDir()` 下的 `<session-id>.ckpt/`,每个 checkpoint 一个 JSON 文件并附带小型索引。这样删除成本低,单个快照损坏也只影响自身。它与消息 JSONL(`agent.Session.Save`)分开,因此无需改动会话格式。
- **跨进程保留**:恢复会话时会重新加载 checkpoint,重启后仍可 rewind,与 Claude Code 保持一致。
- **保留策略**:随会话清理,默认约 30 天并可配置,以限制完整内容快照占用的磁盘空间。
## Controller API:两个前端共用的统一入口
Checkpoint 位于 `control.Controller`,与 `SetPlanMode`、`Compact`、`NewSession` 并列。终端 TUI、桌面 WebView 和 HTTP/SSE server 都通过同一入口触发 rewind,不在各自前端重复实现逻辑。
```go
type RewindScope int // Code | Conversation | Both
func (c *Controller) Checkpoints() []CheckpointMeta // for the picker
func (c *Controller) Rewind(turn int, scope RewindScope) error
```
- **Code**:遍历从 `turn` 到最新的所有 checkpoint,按路径选取最早的 `FileSnap`,把文件恢复到对应内容;若为 `nil` 则删除。也就是撤销 `turn` 及之后的全部编辑。恢复前会再次按当前工作区根目录检查路径逃逸。
- **Conversation**:把 `Session.Messages` 截断到 `turn` 对应用户消息之前,重新 `Save`,并发送替换后的历史事件供前端重绘。所选回合的提示会回填到输入框,方便修改后重发。
- **Both**:同时恢复代码与会话。
统一的 `Rewound` 事件(或复用 history-replace 事件)让所有前端以相同方式重绘。
## CLI 体验(与 Claude Code 对齐)
- 输入框为空时按两次 **`Esc`**,或执行 **`/rewind`**,打开用户回合列表,显示时间和每个回合改动的文件。`chat_tui` 已跟踪双 Esc 的时间窗口。
- 选择一个回合后显示子菜单:**`[code+conversation] [conversation] [code] [cancel]`**。
- 恢复 conversation 或 both 时,把所选提示回填到输入框。
## 桌面端体验(与 VS Code 扩展对齐)
- Transcript 中每条用户消息悬停时显示 **rewind** 控件,并提供:恢复代码、恢复会话、同时恢复、从此处分叉。
- 前端通过 Wails binding 调用同一个 `controller.Rewind`;Controller 事件流推送恢复结果,React 负责重绘。前端不包含独立 rewind 逻辑。
## 非目标与边界情况
- **Bash / 外部副作用**:`rm`、`mv`、数据库写入、部署等不会被跟踪,也无法通过 rewind 撤销,这与 Claude Code 一致。
- **回合之间的外部编辑**:快照保存的是回合开始时的内容,因此恢复会覆盖期间由 Reasonix 之外产生的改动。
- **删除**:编辑工具执行的删除可以恢复,因为快照保存了原内容;`bash rm` 无法恢复。
- **大文件**:当前保存完整快照,由保留策略控制磁盘占用;若成为问题,再引入按内容寻址的去重。
## 阶段划分
1. **Phase 1**:快照存储、`executeOne` 捕获切入点、`Controller.Rewind`(code / conversation / both)、CLI 选择器(Esc-Esc + `/rewind`)。
2. **Phase 2**:桌面端悬停 rewind、“从此处分叉”、“从此处开始摘要 / 摘要到此处”,以及可选的 git-backed 模式。
## 待确认问题
- 是否在 `/compact` 和 `NewSession` 边界创建快照?
- 默认保留窗口应为多久,是否需要在 `[checkpoints]` 配置中公开?
- 是否从一开始就使用内容寻址去重,而不是每个快照一个文件?
-132
View File
@@ -1,132 +0,0 @@
# 协作方式与运行模式
Reasonix 桌面端输入框左下角的菜单包含两条互相独立的轴:
- **计划模式**:要求模型先产出计划,确认后再切换到实施。
- **目标模式**:给 Reasonix 一个目标,让它持续推进直到完成、阻塞或停止。
- **运行模式**:在“均衡”与“交付”两档之间选择交付质量倾向。
计划模式和目标模式属于“协作方式”轴,通常二选一;运行模式是独立轴,可以和普通聊天、计划模式、目标模式任意组合。
## 计划模式
计划模式适合在动手前先确认方案。开启后,Reasonix 会收到“先规划、不要开始实施”的工作流指令,读取必要上下文、分析问题并给出计划。它不是权限边界:规划期间的任何工具调用仍由当前 Ask/Auto/Yolo、权限规则与 Sandbox 决定;`complete_step` 等显式执行阶段工具会等到计划批准后。
### 怎么开启
- 点击输入框左下角的“协作方式”按钮,选择“计划”。
- 也可以使用 `Shift+Tab` 切换计划模式。
- 开启后输入框下方会显示“计划”标签;点击该标签或再次使用 `Shift+Tab` 可退出。
### 建议使用场景
- 你还不确定实现方案,希望先看 Reasonix 的拆解。
- 改动范围可能跨多个文件、模块或配置。
- 需要先评估风险、测试面、兼容性或发布影响。
- 你希望先让 Reasonix 阅读代码和文档、给出方案,再决定是否继续实施。
### 注意事项
- 计划模式不是“自动完成任务”。它会先暂停在计划阶段,等待你确认。
- 计划模式通过模型指令和确认步骤减少误改风险,但不替代 Ask、`deny` 规则或 Sandbox。
- 如果你已经明确要直接改一个小问题,普通模式通常更快。
- 计划模式只控制“先规划再执行”的流程,不决定成本或交付倾向;可按任务选择任一运行模式。
- Ask 不是只读模式:需审批的 writer 在批准后仍可执行。需要技术上严格只读时,应使用显式只读 subagent/权限配置,而不是依赖 Plan 或 Ask。
## 目标模式
目标模式适合给 Reasonix 一个更长线的目标,让它持续推进。目标启动后,Reasonix 会围绕该目标工作,直到任务完成、遇到阻塞、被你停止,或需要你确认关键决策。
### 怎么开启
- 点击“协作方式”按钮,选择“目标”。
- 如果输入框里已有文字,选择“目标”会把当前文字作为目标启动。
- 如果输入框为空,选择“目标”后会进入目标输入状态,输入目标并发送即可启动。
- 开启后输入框下方会显示“目标”标签;点击该标签可退出目标模式。
### 建议使用场景
- 你希望 Reasonix 连续完成一组相关步骤,例如“修复这个问题并补测试”。
- 任务需要探索、实现、验证多个阶段。
- 你希望减少中途反复下指令,让 Reasonix 在目标范围内持续推进。
- 目标明显是长周期研究、排障或优化,例如“持续排查直到根因明确”“彻底实现并验证”
“不要原地打转”。Goal 默认持续执行,不会因为固定轮数或无进展计数而暂停。
### 推荐目标写法
复杂目标可以写成一份任务合约:Context、Request、Output format、Constraints、Pause policy。
Goal 模式会把这些部分当作任务边界;除非下一步涉及不可逆或对外可见操作、任务范围变化,
或需要你提供信息,否则会继续完成任务后再汇报。完整模板见
[`TASK_CONTRACT.zh-CN.md`](./TASK_CONTRACT.zh-CN.md)。
### 注意事项
- 目标要写得具体。推荐包含范围、成功标准和限制,例如“只改桌面端输入栏,补前端测试,不改后端协议”。
- 目标模式不是跳过审批。遇到高风险操作、权限限制、阻塞或需要产品判断时,仍可能停下来询问。
- 如果目标过大或边界不清,Reasonix 可能需要更多探索轮次,也会消耗更多 token。
- 普通聊天不会因为目标文本看起来复杂或长周期而自动切换模式。只有明确选择“目标”或使用
`/goal` 后,Reasonix 才会进入 Goal;旧简单/写入/研究参数仍可解析,但不再改变额度。
- Goal 没有默认轮数、turn 数或时间上限。`--max-steps`、正数时间/成本预算仍可由用户显式选择。
- 旧 `.reasonix/autoresearch/...` 目录不会被新版本创建或改写;显式引用旧任务路径时只会读取并恢复成普通 Goal。
- 目标模式和计划模式是同一协作轴。切到计划模式时,会退出目标草稿/目标显示状态;运行模式不会因此改变。
## 执行设定(Agent Preset)
执行设定决定会话的宿主规划 / 验证 / 独立复审策略。三种执行设定共享同一套 provider
可见核心工具面(直接编码工具 + 后台 shell 生命周期 + 稳定的 `use_capability` 代理);
可选能力一律经 `use_capability` 调度,不扩展 top-level schema。每个标签页独立保存选择;
会话内切换就地更新执行设定,不重建 Controller。
### 均衡 · 默认(Balanced)
Balanced 是默认档,对应旧持久化值 `full`。按风险自动选择轻/全规划,分档验证,中风险
多文件变更可条件触发独立复审。
- 适合大多数普通开发与聊天任务。
- `--preset balanced` 和不传 `--preset` 的运行语义不变。
- 旧 session/tab 中空值或 `full` 都会继续解释为 Balanced。
### 交付 · 完整验证(Delivery)
Delivery 使用与其他档相同的统一工具面,并增加稳定交付合约:明确验收标准;条件允许时先
复现;检查项目规则和相关代码;修复根因;运行聚焦验证;复审 diff 与相邻行为;没有证据时
不宣称成功。
- 适合编码、修 bug、跨文件实现和需要可靠交付证据的任务。
- 通常会使用更多模型调用和 token,耗时也可能更长。
- 宿主会在变更或验证命令前检查是否已有具体的 `todo_write` 验收清单;缺失时直接阻止。
- 发生变更后,宿主会要求在最后一次变更之后复查结果、运行成功的验证命令,并用
`complete_step` 正式签收。
- Skill/MCP 的 `require`/`prefer` 路由由宿主门禁强制。
- 中/高风险改动会强制运行结构化 `review` / `security_review`。
### 怎么选择
- 桌面端点击输入框左下角菜单,在“执行设定”下选择“均衡”或“交付”。
- CLI 启动时使用 `reasonix --preset balanced|delivery`,非交互运行使用
`reasonix run --preset ...`。兼容 `--profile economy|balanced|delivery`。
- TUI 会话内使用 `/preset balanced|delivery` 热切换;不带参数的 `/preset` 会列出
三档并标记当前项。`/work-mode` 与 `/profile` 保留为兼容别名。
- 会话内切换保留 history、session 路径、审批/Yolo 状态,不重建 Controller。当前 turn、
审批/询问或后台任务仍在运行时不能切换。
- `/preset` 只修改当前会话,不写入全局默认值。三种执行设定共享统一工具 schema,切换不会
制造新的工具面缓存前缀。
## 协作方式与 Profile 如何组合
| 组合 | 是否支持 | 说明 |
| --- | --- | --- |
| 普通 + 均衡 | 支持 | 日常聊天与开发的默认组合。 |
| 计划 + 均衡 | 支持 | 先用完整工具面研究并确认方案,再执行。 |
| 目标 + 交付优先 | 支持 | 持续推进明确目标,并强调实现与验证闭环。 |
| 计划 + 目标 | 不建议同时使用 | 两者都是协作方式轴,切换计划会退出目标草稿/目标显示状态。 |
| 工具权限(询问/自动/Yolo)+ 任一组合 | 支持 | 工具权限控制工具审批;Sandbox 继续控制文件、进程与网络边界。 |
工具权限的详细区别和使用场景,见 [`TOOL_APPROVAL_MODES.zh-CN.md`](./TOOL_APPROVAL_MODES.zh-CN.md)。
## 推荐选择
- **不确定怎么选**:保持均衡和普通模式。
- **担心 Reasonix 改错**:开启计划模式先确认方案,并保持 Ask 与合适的 Sandbox 边界。
- **想让 Reasonix 持续推进一个明确目标**:开启目标模式,目标写清楚成功标准。
- **编码、修 bug 或复杂实现更看重最终质量**:选择交付优先,可再搭配目标模式。
-242
View File
@@ -1,242 +0,0 @@
# 配置路径
从 **Reasonix v1.8.1** 开始,Reasonix 使用一个用户可见的全局目录存放配置和用户状态。CLI 与桌面端共用这个目录。
## Reasonix Home
| 平台 | Reasonix home |
| --- | --- |
| macOS | `~/.reasonix` |
| Linux | `~/.reasonix` |
| Windows | `%APPDATA%\reasonix` |
可以设置 `REASONIX_HOME` 覆盖 Reasonix home,主要用于测试、CI 或便携安装。普通用户通常不需要设置。
设置 `REASONIX_HOME` 后,运行时会变成完整自包含模式:配置、状态、缓存和数据都会位于该目录树下。
Legacy 迁移、OS home 约定目录扫描以及其他 fallback 路径都会跳过,避免从系统级正式安装带入或写回数据。
高级测试或便携安装可以设置 `REASONIX_STATE_HOME` 来移动 sessions、archive、memory 等运行状态。
它不会移动全局配置或 provider 凭据;这些仍然位于 `REASONIX_HOME` 下。如果旧版本曾把 provider key
写到 `REASONIX_STATE_HOME/.env`,Reasonix 会在 `<Reasonix home>/.env` 缺少对应 key 时非破坏性导入。
## 目录内容
| 数据 | 路径 |
| --- | --- |
| 全局配置 | `<Reasonix home>/config.toml` |
| 全局 provider 凭据 | `<Reasonix home>/.env` |
| 旧 credentials 导入来源 | `<Reasonix home>/credentials` |
| 全局斜杠命令 | `<Reasonix home>/commands/` |
| 全局 skills | `<Reasonix home>/skills/` |
| 全局 hooks | `<Reasonix home>/settings.json` |
| 远程 SSH 托管 known_hosts | `<Reasonix home>/remote/known_hosts` |
| 会话 | `<state root>/sessions/` |
| 归档 | `<state root>/archive/` |
| 记忆 | `<state root>/memory/` 与 `<state root>/projects/` |
| 可丢弃的会话 Catalog | `<cache root>/session-catalog/v3.sqlite` |
| 可丢弃的 Task Catalog | `<cache root>/task-catalog/v1.sqlite` |
`<state root>` 默认等于 `<Reasonix home>`;只有设置 `REASONIX_STATE_HOME`
时才会不同。
JSONL、event log、metadata sidecar 和 `desktop-projects.json` 是权威的会话数据,
列出或打开一个会话不读其它任何东西。Task snapshot 和
event log 也是权威数据。
全局用户配置文件名是 `config.toml`。项目本地配置文件仍叫 `reasonix.toml`。
如果有人说“全局 reasonix.toml”,通常指的是 `<Reasonix home>/config.toml`。
## 全局 `config.toml`
`<Reasonix home>/config.toml` 存放 CLI 与桌面端共用的非密钥配置。它可以包含
Reasonix 写入用户配置的 provider、plugin、UI、desktop、tool、skill、sandbox、
bot 和 agent 设置。Provider 条目只保存 `api_key_env` 里的凭据变量名,不保存真实密钥值。
已保存的 provider 与 bot 凭据变量不会进入任何由模型控制的子进程环境。Reasonix 的
文件读取工具、受沙盒保护的 shell 命令和 MCP server 也无法读取全局凭据 `.env`;
项目自身的普通 `.env` 可见性保持不变。Windows 的 shell 命令仍不具备 OS 级沙箱,
详见《使用指南》,因此只应为可信任务批准 shell 权限。
示例:
```toml
config_version = 1
default_model = "deepseek/deepseek-flash"
language = "zh"
credentials_store = "auto" # 旧兼容字段;provider key 保存在 .env
[ui]
theme = "auto"
cursor_shape = "bar" # CLI/TUI 输入光标:underline|block|bar
show_turn_usage = false # 隐藏 TUI 每轮 token/费用回执;默认 true
[desktop]
provider_access = ["deepseek"]
[[providers]]
name = "deepseek"
kind = "anthropic"
base_url = "https://api.deepseek.com/anthropic"
models = ["deepseek-flash", "deepseek-v4-pro"]
default = "deepseek-flash"
api_key_env = "DEEPSEEK_API_KEY"
web_search = true
[[plugins]]
name = "example"
command = "example-mcp-server"
```
不要把 API key 的真实值写进 `config.toml`。这个文件是普通配置:可以查看、编辑、
迁移,也可以在常规脱敏后用于诊断。密钥值属于下面的全局 `.env`。
`[ui].cursor_shape` 只影响 CLI/TUI 的输入框。默认值 `bar` 清晰可见,同时不会覆盖
CJK 双宽字符;如果偏好其它形状,可以设为 `block` 或 `underline`。
`[ui].show_turn_usage = false` 会隐藏 TUI transcript 中每次模型请求完成后的 token 与
费用回执;统计和运行中状态仍正常更新。默认值为 `true`。
### 自定义 provider 的 `api_key_env` 命名
通过桌面端设置或 `reasonix setup` 添加自定义 provider 时,Reasonix 会把生成的
`api_key_env` 保存到 `config.toml`,并把真实密钥值写入全局 `.env` 中同名的 key。
生成结果是稳定的,因此同一个 provider 重启后仍会读取同一个凭据槽位。
Reasonix 会根据 provider 名称生成默认值。能规范化成 ASCII 的名称会得到可读的
env 名,例如 `LOCAL_GATEWAY_API_KEY`;如果名称全部由中文等非 ASCII 字符组成,则会
生成带稳定 hash 后缀的名称,例如 `CUSTOM_d39b9067_API_KEY`,避免多个中文 provider
都共用 `CUSTOM_API_KEY`。如果名称以数字开头,则会添加 `CUSTOM_` 前缀以保证生成的
环境变量名合法;例如 `9router` 会生成 `CUSTOM_9ROUTER_API_KEY`。
CLI 的自定义 provider 向导会先根据 base URL 生成 provider 名称,再套用同一套
provider-name 规则。例如 `https://token.sensenova.cn/v1` 会生成 provider 名
`custom-token-sensenova-cn`,默认 key env 是 `CUSTOM_TOKEN_SENSENOVA_CN_API_KEY`。
直接回车会接受这个默认值;如果你确实想让多个 provider 共用一个凭据,也可以手动输入
`CUSTOM_API_KEY` 或其他自定义 env 名。
升级时不会自动改写已有配置。旧配置中已经使用 `CUSTOM_API_KEY` 的自定义 provider 会继续
读取这个 key。若多个旧自定义 provider 已经意外共用了 `CUSTOM_API_KEY`,需要手动把各自的
`api_key_env` 改成不同名称,并重新保存对应的 API key。
### 自定义 provider 的端点 URL
桌面端自定义 provider 表单会把「API 地址」作为完整请求地址写入 `request_url`,
Reasonix 不会追加或改写路径。已有 TOML 配置不会被重新解释:旧 `chat_url` 继续
保持原来的 OpenAI 专用行为,Anthropic 和 Responses 仍会根据 `base_url` 推导请求
路径;只有用户在新版桌面端明确保存该 provider 后,才会写入并启用 `request_url`。
保存 OpenAI-compatible provider 时还会把完整地址同步到旧 `chat_url`,使旧版本
继续使用同一请求目标。旧版本无法识别 Anthropic 或 Responses 的任意自定义请求路径。
模型发现需要单独地址时可设置 `models_url`;否则 Reasonix 会继续从 `base_url`
推测模型发现地址。
## 全局 `.env`
`<Reasonix home>/.env` 是 Reasonix 保存的 provider API key 的唯一运行时来源。
setup 向导、桌面端设置页、CLI 缺 key 提示以及删除 provider key 的操作,都会通过同一套凭据 helper 读写这个文件。
结构:
```dotenv
DEEPSEEK_API_KEY=sk-...
GEMINI_API_KEY=...
ANTHROPIC_API_KEY=...
# reasonix-cleared OLD_API_KEY
```
规则:
- 每行一个 `KEY=value`;
- 空行和 `#` 注释会被忽略;
- 读取时接受 `export KEY=value` 和带引号的值;
- Reasonix 写入时会拒绝多行值;
- key 必须是类似 `DEEPSEEK_API_KEY` 的 shell 风格变量名;
- `# reasonix-cleared KEY` 是删除 key 后写入的非密钥标记,用来防止旧存储把它静默迁回;
- 在操作系统支持的情况下,Reasonix 会用受限权限写入该文件。
Provider 请求只会从这个全局 `.env` 解析 key。项目 `.env`、home `.env`、继承的 shell
环境变量、旧 `credentials` 文件和系统 keyring 都不再作为运行时 provider key fallback。项目 `.env`、home `.env` 和继承的 shell 环境变量不会自动导入到全局凭据文件。
旧 `credentials` 文件和旧 keyring 条目只会在新全局 `.env` 缺少对应 key 时作为非破坏性迁移来源读取。
项目 `.env` 仍会作为当前 workspace 范围内的非 provider 变量展开来源,例如 MCP/plugin 的 env、headers、URL、command 和 args 中的 `${VAR}`;这些值不会写入进程环境,`REASONIX_HOME`、`REASONIX_STATE_HOME`、`XDG_CONFIG_HOME` 等 Reasonix 控制变量也会被忽略。
缓存仍放在系统缓存目录,例如 macOS 的 `~/Library/Caches/reasonix`、
Linux 的 `$XDG_CACHE_HOME/reasonix` 或 `~/.cache/reasonix`、Windows 的
`%LOCALAPPDATA%\reasonix\cache`。可以设置 `REASONIX_CACHE_HOME` 覆盖缓存根目录。
设置 `REASONIX_HOME` 后,缓存会放在 `$REASONIX_HOME/cache`;如果同时设置
`REASONIX_CACHE_HOME`,后者优先。
## 配置优先级
运行时配置按下面顺序解析:
```text
命令行参数
> 项目 ./reasonix.toml
> 全局 <Reasonix home>/config.toml
> 兼容读取的旧全局配置
> 内置默认值
```
写配置时始终写入新的全局路径:
```text
macOS/Linux: ~/.reasonix/config.toml
Windows: %APPDATA%\reasonix\config.toml
```
## 旧路径迁移
从 **v1.8.1** 开始,Reasonix 启动时会在第一次加载配置前自动检查旧路径。迁移是同步、一次性、非破坏性的:旧文件会被复制或转换到 Reasonix home,原文件保留。
旧配置来源包括:
```text
~/Library/Application Support/reasonix/config.toml
~/.config/reasonix/config.toml
~/.reasonix/reasonix.toml
~/.reasonix/config.json
```
旧 credentials、memory 文件和 sessions 也会在新目标不存在时导入到 Reasonix home。
旧 provider key 只会在 `<Reasonix home>/.env` 尚未包含同名 key 时复制进去。若新的全局配置已经存在,则新配置优先;旧配置只作为兼容 fallback 保留。
从 **v1.9.1** 开始,Reasonix 还会在升级时把已知旧路径、legacy `config.json`、
桌面端已登记项目和恢复 tabs 对应项目里的 MCP 配置汇总补齐到全局
`<Reasonix home>/config.toml`。已有的全局 `[[plugins]]` 按名称优先,不会被旧
配置或项目配置覆盖;源文件会保留不变。该补齐会写入一次性 marker,避免用户之后
主动删除某个全局 MCP 时又被旧项目配置反复恢复。
## 手动补救迁移
如果 Reasonix 已经创建了新的 home 目录,但当时旧数据还不在可扫描路径里;或者先打开了桌面端,导致自动迁移没有把旧路径数据补齐,可以在任一前端运行补救命令:
```text
/migrate
```
在 CLI TUI 中,把 `/migrate` 输入到聊天输入框。在桌面端中,把同一个命令输入到 composer。命令会显示进度提示:
1. 检查旧配置和 credentials;
2. 扫描已知旧 memory 位置;
3. 扫描已知旧 sessions 目录;
4. 导入尚未迁移过的 memory 文件和 sessions;
5. 输出最终汇总。
如果旧 v0.x sessions 不在上述已知旧路径里,例如 Windows v0.52 安装时选择了自定义安装/数据目录,可以显式指定旧目录:
```text
/migrate --from "D:\OldReasonix"
```
显式形式只导入 sessions。这个路径可以是旧安装目录、`.reasonix`/数据目录,或者
`sessions` 目录本身;Reasonix 会在该根目录下检查常见布局,并使用按来源目录区分的
marker,因此之前已经运行过普通 `/migrate` 也不会挡住这次后补导入。
该补救命令仍然是非破坏性的。它不会覆盖已有的
`<Reasonix home>/config.toml`;如果新配置已经存在,需要手动把旧配置里缺失的设置复制过去。旧 memory 文件只会在目标文件不存在时复制。它也会尊重 session 导入 marker,因此已经迁移过、之后又被用户删除的会话,不会在后续 `/migrate` 中被重新恢复。
版本限制:
- 自动迁移从 **v1.8.1** 开始。
- `/migrate` 只存在于包含该命令的 Go 版 Reasonix 构建中。如果 Reasonix 提示 `unknown command`,请先升级后再运行。
- legacy `0.x` TypeScript 线没有这个命令。
- 普通 `/migrate` 只会重新扫描上面列出的旧路径。只有确认某个目录是 v0.x session 来源时,才使用 `/migrate --from <path>`;它不是备份恢复工具或降级导入工具。
@@ -1,82 +0,0 @@
# Windows / Linux Desktop 崩溃诊断运行手册
<a href="./DESKTOP_CRASH_DIAGNOSTICS_RUNBOOK.md">English</a>
本手册用于跨平台 Desktop 诊断链路的发布、隐私、性能和根因闭环。Windows build
`17763` 是重点实验环境,不是代码白名单;发布诊断版本本身不代表问题已经解决。
## 发布顺序
1. 冻结唯一候选 SHA,已发布 tag 不得移动或重建。
2. 备份 D1,并先用 `PRAGMA table_info` 检查生产,再应用
`workers/crash-report/migrate-diagnostics-v2.sql`。若 draft 字段已提前存在,停止发布,
另做纯加法 reconciliation migration。
3. 验证 `report_daily`、`report_installations`、
`report_event_dimensions`、`diagnostics_meta`、fingerprint/date 索引、ping
窗口索引,以及 `installation_linked_since`。
4. 先部署 Worker;用旧 Report/Ping/Metrics、legacy `webview2`、Windows/Linux
`webRuntime` payload 做 `channel=test` smoke。
5. 用同一 SHA 生成签名 Windows/Linux 构建;能力矩阵和性能门禁通过后才发布 feature
release。
6. 通过管理界面保留审计地整理历史数据:忽略 `[go panic] safe` / `v9.9.9`,将
`72daba81` 标记为在 `desktop-v1.19.3` 解决,忽略旧
`desktop.abnormal_exit` replay 分组。
## 隐私与兼容 smoke
旧 payload 可以缺失所有新增字段;legacy `webview2` 必须归一化为 `webRuntime`。
同一 engine/kind/reason/exit code 的恢复成功与失败必须属于同一 fingerprint。还需验证:
- 原始 install ID 不进入样本、HTML、应用/审计日志、导出和 pending 文件;
- 模块只保留 basename;不包含内容、密钥、账号、hostname、完整路径、GPU 型号和驱动;
- 重复事件正确累加 daily/install/event-dimension,且早期环境组合不被覆盖;
- 删除测试分组会删除三张诊断聚合表的对应数据;
- 诊断事实、ping、metric user 按 30 天分块清理;
- `channel=test` 始终位于 development namespace。
## 正常体验门禁
候选版本在 Wails 启动前只允许一次本地配置读取、一次非阻塞归属锁和一次小型原子生命周期
写入。Runtime 探测及报告/指标落盘必须在 Wails 启动后或有界后台消费者中执行;COM/GTK
回调只能非阻塞入队或递增原子丢弃计数。任何诊断失败都必须 fail-open。
使用同一 SHA 与关闭诊断的基线比较:诊断初始化 p95 不超过 10 ms、p99 不超过 25
ms;DOM-ready p95 回归不超过 `max(20 ms, 2%)`;shutdown p95 回归不超过 20 ms;
空闲 CPU 增幅小于 0.1 个百分点;RSS 增幅不超过 2 MiB。30 分钟正常使用期间必须是:
0 次诊断 reload、0 个轮询 timer、0 个新增弹窗,除已有 ping/metrics 外 0 个额外请求。
## 能力认证矩阵
全过程使用同一候选 SHA。Runtime、GPU 与驱动信息只记录在私密实验表,客户端不采集驱动。
| 平台 | 必须覆盖 |
| --- | --- |
| Windows 10 LTSC 2019 `17763` | VM + 实体 GPU;系统及最新 Evergreen WebView2;GPU 开/关 |
| Windows 10 `19045` | x64 对照;系统及 Evergreen WebView2 |
| Windows 11 稳定版 | x64 实机及当前稳定 Runtime |
| Windows arm64 | 正式交叉构建 + 一台设备 smoke |
| Ubuntu 22.04 | WebKitGTK 4.0、X11 |
| Ubuntu 24.04 | WebKitGTK 4.1、X11、Wayland |
| Debian 12、Fedora stable、Arch rolling | 能力 smoke;本地会话;Intel/AMD/NVIDIA 代表性覆盖 |
| 远程会话 | Windows RDP 与 Linux remote/xrdp |
每个环境执行 20 次冷启动/正常退出、10 次更新重启、60 分钟工作负载、50 次最小化/
恢复,以及休眠、显示器/DPI、远程连接切换。测试构建可定向终止 renderer/web process,
验证只恢复一次。Windows 收集 WER/可靠性监视器,Linux 收集 journal/coredump 元数据。
dump/core 仅在用户明确授权后私密传输,并在分析后删除。
## 根因和观察闭环
环境关联至少满足一项:两台同类实验节点复现且对照不复现;或三个不同线上安装命中同一
fingerprint,同时该环境至少有 30 个活跃安装,影响率达到对照 3 倍。GPU workaround
要求每台 GPU-on 至少 `2/20`、两台 GPU-off 合计 `0/40`,且每台两小时长测为 0。
workaround 必须按已有能力/Runtime 证据限定,不能只按发行版名称或 Windows build。
Integrity failure 转签名、注入和安全软件调查;OOM 转内存与会话资源调查;Runtime
聚集才支持后续最低版本或升级策略。renderer 恢复成功不算应用崩溃;只有 lifecycle
abnormal exit 时,必须拿到 WER、journal、dump 或 core 之一才能结案。
上线后观察七个完整 UTC 日:身份覆盖率目标 95%;低于 90% 不展示精确影响率。每天检查
legacy replay、fatal/recovered/degraded 数量关系、recovery failure、平台/Runtime/GPU
影响率、D1 增长、retention 和查询耗时。证据不足就保持 open 并延长至 30 天。只有根因
被证实后才发布定向补丁,修复后实验室要求 `0/40` 复现。
-262
View File
@@ -1,262 +0,0 @@
# 桌面端 Hooks 使用说明
<a href="../README.zh-CN.md">README</a>
&nbsp;·&nbsp;
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="./SPEC.md">规格</a>
Hooks 让 Reasonix 在会话、用户输入、工具调用、模型返回、压缩上下文等节点执行本地 shell 命令。桌面端在“设置 -> Hooks”里提供图形化编辑入口,本质上读写同一份 `settings.json`。
> Hook 命令会在本机执行 shell。全局和项目 hooks 都会从各自配置位置自动加载。
## 快速开始
1. 打开桌面端“设置 -> Hooks”。
2. 选择范围:
- “全局”:保存到 `<Reasonix home>/settings.json`,始终加载;Windows 默认是 `%APPDATA%\reasonix\settings.json`,macOS/Linux 默认是 `~/.reasonix/settings.json`。
- “项目”:保存到当前工作区的 `.reasonix/settings.json`,打开项目时自动加载。
3. 在 JSON 配置框里编辑 `hooks`。
4. 保存后,重启桌面端,让新配置进入会话。`/new` 只开启新对话,不会重新读取 hooks 配置。
示例:
```json
{
"hooks": {
"PreToolUse": [
{
"match": "bash",
"command": "node .reasonix/hooks/check-bash.js",
"description": "Block dangerous shell commands",
"timeout": 5000
}
],
"Stop": [
{
"command": "echo Reasonix turn finished"
}
]
}
}
```
## 配置文件位置
| 范围 | 文件 | 加载方式 | 加载顺序 |
| --- | --- | --- | --- |
| 全局 | `<Reasonix home>/settings.json` | 自动 | 项目 hooks 之后 |
| 项目 | `<workspace>/.reasonix/settings.json` | 自动 | 全局 hooks 之前 |
同一个事件下,项目 hooks 先运行,全局 hooks 后运行;同一范围内按数组顺序运行。阻塞型事件遇到第一个阻塞 hook 后,会停止继续执行后面的 hook。
## 配置 JSON 格式
推荐写法是一个带 `hooks` 字段的对象:
```json
{
"hooks": {
"PreToolUse": [
{ "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" }
],
"UserPromptSubmit": [
{ "command": "node ~/.reasonix/hooks/check-prompt.js" }
],
"Stop": [
{ "command": "osascript -e 'display notification \"Turn done\" with title \"Reasonix\"'" }
]
}
}
```
桌面端 JSON 编辑器也接受两种便捷输入,保存前会格式化回 `{"hooks": ...}`:
```json
{
"PreToolUse": [
{ "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" }
],
"Stop": [
{ "command": "echo done" }
]
}
```
```json
[
{ "event": "PreToolUse", "match": "bash", "command": "node .reasonix/hooks/pre-tool.js" },
{ "event": "Stop", "command": "echo done" }
]
```
每个 hook 对象支持这些字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `command` | string | 必填。通过平台 shell 执行的命令。空字符串会被忽略。 |
| `match` | string | 仅 `PreToolUse`、`PostToolUse` 使用。锚定正则,空字符串或 `*` 表示匹配所有工具。 |
| `description` | string | 可选。显示在 hooks 列表或设置页里的说明。 |
| `timeout` | number | 可选。毫秒数。未设置时,阻塞型事件默认 5000ms,其它事件默认 30000ms。 |
| `cwd` | string | 可选。覆盖 hook 命令工作目录。默认使用当前会话的 `cwd`。 |
`match` 是锚定正则:`"file"` 不会匹配 `read_file`,需要写成 `".*file"`。正则非法时该 hook 不会触发。
`command` 默认通过平台 shell 执行:macOS/Linux 使用 `sh -c`,Windows 使用
`cmd /c`。Windows 上如果命令本身指向带 POSIX shell shebang 的脚本文件,Reasonix
会自动改用 Git Bash;如果 Windows hook 自己显式写了裸命令 `sh -c` 或 `bash -c`,Reasonix
会查找 Git for Windows 自带的 Bash 并直接使用它;带目录的显式解释器路径保持不变。
通过 `[tools.shell]` 配置的自定义 Bash 路径同样会被 Hook 复用;找不到 Git Bash 时,
插件 Doctor 和能力诊断会提前显示可操作的依赖提示。Hook stdout/stderr 中的 Windows 旧代码页
文本会转换为 UTF-8,避免中文错误信息显示成乱码。stdin 是 Reasonix 写入的一行 JSON,
见下面的 payload 表。
## 配置里的事件 key
下面这些字符串就是 `hooks` 对象里的事件 key,也是在数组写法中 `event` 字段的取值:
| 事件 key | 触发时机 | 是否可阻塞 | stdout 特殊作用 |
| --- | --- | --- | --- |
| `PreToolUse` | 工具权限已通过、工具真正执行前 | 是 | 无特殊作用 |
| `PostToolUse` | 工具执行后,不论成功或失败 | 否 | 无特殊作用 |
| `UserPromptSubmit` | 用户输入提交后、本轮模型调用前 | 是 | 无特殊作用 |
| `Stop` | 一轮对话结束后 | 否 | 无特殊作用 |
| `PostLLMCall` | 模型流式返回完成后,reasoning 入库前 | 否 | exit 0 且 stdout 非空时,用 stdout 替换展示的 reasoning |
| `SessionStart` | 会话第一次变为活跃,或 `/new`、清空后新会话开始 | 否 | stdout 会作为下一轮模型上下文注入 |
| `SessionEnd` | 会话关闭、切换、`/new`、清空或控制器释放时 | 否 | 无特殊作用 |
| `SubagentStop` | 前台 `task` 子代理完成后 | 否 | 无特殊作用 |
| `Notification` | 需要用户注意时,例如等待工具审批 | 否 | 无特殊作用 |
| `PreCompact` | 上下文压缩开始前 | 否 | stdout 会追加为压缩摘要的额外指导 |
只有 `PreToolUse` 和 `UserPromptSubmit` 是阻塞型事件。阻塞型事件中,命令 `exit 2` 或超时会阻断后续执行。
## Hook 命令收到的 payload
Reasonix 会把一行 JSON 写入 hook 命令的 stdin。所有 payload 都至少有:
| key | 类型 | 说明 |
| --- | --- | --- |
| `event` | string | 当前事件 key。 |
| `cwd` | string | 当前会话工作目录,也就是 hook 默认执行目录。 |
其它 key 按事件出现;空值会被省略。
| 事件 key | 额外 payload key | 示例 |
| --- | --- | --- |
| `PreToolUse` | `toolName`, `toolArgs` | `{"event":"PreToolUse","cwd":"/repo","toolName":"bash","toolArgs":{"command":"go test ./..."}}` |
| `PostToolUse` | `toolName`, `toolArgs`, `toolResult` | `{"event":"PostToolUse","cwd":"/repo","toolName":"bash","toolArgs":{"command":"go test ./..."},"toolResult":"ok"}` |
| `UserPromptSubmit` | `prompt`, `turn` | `{"event":"UserPromptSubmit","cwd":"/repo","prompt":"修复测试","turn":1}` |
| `Stop` | `lastAssistantText`, `turn` | `{"event":"Stop","cwd":"/repo","lastAssistantText":"已修复","turn":1}` |
| `PostLLMCall` | `reasoning`, `turn` | `{"event":"PostLLMCall","cwd":"/repo","reasoning":"raw reasoning","turn":1}` |
| `SessionStart` | 无 | `{"event":"SessionStart","cwd":"/repo"}` |
| `SessionEnd` | 无 | `{"event":"SessionEnd","cwd":"/repo"}` |
| `SubagentStop` | `lastAssistantText` | `{"event":"SubagentStop","cwd":"/repo","lastAssistantText":"子代理结论"}` |
| `Notification` | `message` | `{"event":"Notification","cwd":"/repo","message":"approval needed: bash go test ./..."}` |
| `PreCompact` | `trigger` | `{"event":"PreCompact","cwd":"/repo","trigger":"manual"}` |
`toolArgs` 是工具参数的原始 JSON。比如 `bash` 通常会带 `{"command":"..."}`,其它工具会按自己的 schema 传入。`Notification.message` 会做必要的隐私收敛,例如记忆审批只包含工具名,不把记忆正文发给外部通知 hook。
## 退出码和输出
| 结果 | 阻塞型事件 | 非阻塞型事件 |
| --- | --- | --- |
| exit 0 | 通过 | 通过 |
| exit 2 | 阻塞 | 警告 |
| 其它非零退出码 | 警告,不阻塞 | 警告 |
| 超时 | 阻塞 | 警告 |
| 命令启动失败 | 错误提示,不阻塞 | 错误提示 |
stdout 和 stderr 会被捕获、去掉首尾空白,并限制单路输出最多 256KB。非通过结果会显示为 warning,优先展示 stderr,其次展示 stdout。
特殊 stdout 行为:
- `PostLLMCall`:exit 0 且 stdout 非空时,stdout 会替换用户看到的 reasoning。若 provider 的 reasoning 带签名,Reasonix 会保留原始 signed reasoning 用于后续请求,同时仍展示 hook 转换后的文本。
- `SessionStart`:exit 0 且 stdout 非空时,stdout 会作为一次性 `<hook-context event="SessionStart">` 注入下一轮真实用户输入。纯文本 stdout 会原样作为上下文;也可以输出 Claude Code / Codex 兼容 JSON:
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Load the workspace conventions before editing."
}
}
```
`hookEventName` 必须与当前事件一致。该上下文不会写入 system prompt、工具 schema 或项目记忆;它只影响下一轮模型请求。单个 hook 上下文最多保留约 10000 字符,总量最多约 20000 字符,超出会截断并标记。
- `PreCompact`:所有非空 stdout 会按换行拼接,作为本次压缩摘要的额外指导。
- 其它事件:stdout 只在非通过结果中作为提示文本使用,不会自动进入模型上下文。
## 示例:SessionStart 注入启动上下文
```json
{
"hooks": {
"SessionStart": [
{
"command": "printf '%s\\n' '{\"hookSpecificOutput\":{\"hookEventName\":\"SessionStart\",\"additionalContext\":\"Before coding, check the available skills and follow matching workflows.\"}}'"
}
]
}
}
```
这适合把插件或工作流的 bootstrap 说明带入会话。比如 Superpowers 不需要内置到 Reasonix;可以让它自己的 `hooks/session-start-codex` 在 `SessionStart` 输出 `additionalContext`,或让插件根目录 `CLAUDE.md` 被插件包兼容层直接作为 `SessionStart` 上下文读取,Reasonix 会在下一轮把这段说明注入模型上下文。插件包兼容层也会读取 `.claude/settings.json` 里的 command hooks,并按同名事件映射到 Reasonix hooks。Reasonix 默认允许 `max_subagent_depth = 2`,因此 Superpowers 的父会话或第一层 workflow subagent 可以再派发 reviewer/implementer subagent;第二层不会继续获得递归委派工具。若要恢复旧的单层边界,设 `agent.max_subagent_depth = 1`。这会改变子代理可见工具面,可能影响子代理请求的 prompt cache,但不会把 Superpowers 写进 Reasonix 的稳定 system prompt。
## 示例:阻止危险 bash 命令
`.reasonix/settings.json`:
```json
{
"hooks": {
"PreToolUse": [
{
"match": "bash",
"command": "node .reasonix/hooks/block-dangerous-bash.js",
"description": "Block risky bash commands",
"timeout": 3000
}
]
}
}
```
`.reasonix/hooks/block-dangerous-bash.js`:
```js
const fs = require("fs");
const payload = JSON.parse(fs.readFileSync(0, "utf8"));
const command = payload.toolArgs?.command || "";
if (/\brm\s+-rf\b/.test(command) || /\bgit\s+push\b/.test(command)) {
console.error(`blocked dangerous command: ${command}`);
process.exit(2);
}
```
`exit 2` 会让 `PreToolUse` 阻断该工具调用,并把错误信息反馈给界面和模型。
## 示例:压缩前追加摘要重点
```json
{
"hooks": {
"PreCompact": [
{
"command": "printf '%s\n' 'Keep exact user decisions, file paths, and unresolved TODOs.'"
}
]
}
}
```
当自动压缩或 `/compact` 触发时,stdout 会加入摘要指令。
## 排障
- 保存后当前会话没有变化:Hooks 在会话构建时加载。重启桌面端后才会重新读取配置;`/new` 只开启新对话,不会重新加载 hooks。
- 项目 hooks 不执行:确认当前是项目工作区、配置保存在 `.reasonix/settings.json`,并重启 Reasonix 重新加载。也可用只读诊断:`reasonix doctor capabilities` 或桌面端 **设置 → 诊断**(见 [能力诊断](./CAPABILITY_DIAGNOSTICS.zh-CN.md)),关注 `hook.invalid_matcher` / `hook.malformed_settings`。
- `match` 没生效:它只对 `PreToolUse` 和 `PostToolUse` 生效,并且是锚定正则。
- JSON 报 unknown hook event:事件 key 必须完全等于上表的大小写。
- hook 输出太长:每路 stdout/stderr 最多捕获 256KB,超出会截断并显示截断提示。
-147
View File
@@ -1,147 +0,0 @@
# Reasonix 扩展
扩展让插件包在运行时改变 Reasonix 的行为——改写输入、拦截工具调用、
替换系统提示词、提供流式模型 Provider、发布结构化 UI,以及分发
prompts 和主题——全部基于稳定、带版本号的契约。
插件能力分两类:
- **声明式**(任意插件包):skills、agents、commands、prompts、hooks、
MCP servers 和主题。它们是文件与配置,按宿主正常权限运行。
- **代码型 Runtime**(Manifest v2 的 `runtime` 块):通过 Extension
Protocol 驱动的 Sidecar 进程。代码型扩展是**完全信任(full trust)**
的——安装前请务必阅读下文安全章节。
## 安装与管理
扩展的安装方式与普通插件包完全一致:
```bash
reasonix plugin install git:github.com/owner/extension --dry-run # 预览
reasonix plugin install git:github.com/owner/extension --yes # 安装
reasonix plugin show <name> # 详情
reasonix plugin doctor <name> # 校验
```
带有 `runtime` 块的插件,其预览与 `show` 输出会包含 **FULL TRUST**
区块:Runtime 命令、拦截的事件、持有的替换槽,以及 Provider/UI 能力。
安装、更新、替换或 `--link` 即代表授权——没有二次确认,`--link` 在内容
变化后自动保持信任。请只安装你完全信任的运行时。
## 扩展能做什么
- **拦截器(Interceptors)**——观察并裁决 17 个 hook 点(输入、工具
调用、权限判定、Provider 请求/响应、压缩、会话生命周期、前端事件)。
拦截器可以 `continue`、`block`(给出用户可见原因)或 `replace`
(替换载荷);宿主会对每个替换重新校验。
- **替换策略**——单 owner 槽位(`system_prompt`、`context`、
`provider_request`、`provider_response`、`compaction`、
`session_policy`、`permission`、`frontend_events`、`tool:<name>`、
`provider:<ref>`)。同一槽位在所有已安装插件中只能有一个 owner,
争用会令运行时构建失败并列出来源。
- **流式 Provider**——新模型以 `plugin/<plugin>/<provider>/<model>`
出现在模型选择器中,流式语义(text/reasoning/工具调用/usage)与
内置 Provider 一致。该 ref 可用于任何内置 ref 可用之处:
`default_model`、`--model`、CLI/Desktop/ACP 模型选择器以及会话中的
模型切换——包括首次启动。
- **结构化 UI**——status、card、form、notification 在 CLI transcript、
Desktop 与 ACP 客户端中原生渲染(不支持时退化为文本),action 同时
出现在 `/<plugin>:<action>` 斜杠菜单、Desktop 命令面板和 ACP 可发现
命令中。
- **Prompts 与主题**——`/<plugin>:<name>` 提示词模板,以及 Desktop
设置中的只读插件主题(`plugin:<plugin>:<theme>`)。
## 运行时重载
已安装扩展发生变化(安装、更新、启用/禁用、`--link` 内容变化)绝不会
修改正在运行的回合。所有交互前端都提供失败原子的重载入口——CLI
`/reload`、Desktop「重载运行时」(命令面板)、Serve `/reload`、ACP
vendor method `_reasonix.io/session/reloadExtensions`:
1. 回合或后台任务运行中,CLI/Desktop/ACP 只排队一次;Serve 会拒绝本次
请求,由浏览器在空闲后重试。
2. 空闲后启动新 Sidecar 并构建新的运行时快照。
3. 完整成功后原子交换,并迁移 session path、transcript、授权记录和
goal/recovery 状态。
4. 新构建失败时,旧运行时不受影响继续可用。
5. 交换完成后才关闭旧 Sidecar。
每个回合自始至终(含工具批次与压缩)固定使用同一个运行时
generation——扩展变更从下一个回合生效;no-op 重载后 Provider 提示词
缓存前缀字节不变。
## 性能与提示词缓存
未安装代码型 Runtime 时,Agent 仍走原有 nil-dispatcher 路径:不会启动
Sidecar,也不会发生 JSON 编码、RPC 或事件排队。安装 Runtime 后,Reasonix
在同一个 generation 的 30 秒总启动预算内最多并行初始化 4 个 Sidecar;
卡住的可选 Runtime 不会再按已安装包数量成倍拉长启动或 reload。未能在
预算内启动的包按其 `runtime.required` 设置降级或令构建失败。启用后的
同步拦截器会串行进入相应热路径,因此 RPC 与处理耗时会累加;输入、工具、权限和
Provider 拦截器应保持轻量且结果确定。观察事件通过有界非阻塞队列投递,
背压时告警并丢弃,不会卡住当前回合。
纯观察扩展不会改变 Provider 可见缓存前缀。稳定的系统提示词或工具替换
会在安装/重载后产生一次预期的冷前缀,之后仍可持续命中缓存;若策略把
时间戳、随机值、session ID 或其他逐回合动态数据写入系统提示词、工具
Schema、上下文前缀或 Provider 请求,则可能破坏缓存复用。动态数据应尽量
留在当前回合尾部。维护者可用以下命令测量宿主开销:
```bash
go test ./internal/extension/... -run '^$' -bench 'Extension|Dispatch' -benchmem
```
## 开发扩展
建议从完整的
[`starterextension`](../sdk/go/examples/starterextension/README.zh-CN.md)
开始。它把 Manifest、Sidecar 源码、跨平台构建命令、链接安装和第一个可观察
拦截效果放在同一目录。标准开发流程是:
1. 在 `reasonix-plugin.json` 中加入
`apiVersion: "reasonix.io/plugin/v2"`,声明 `contributes` 与
(可选的)`runtime`——见
[插件包文档](./PLUGIN_PACKAGES.zh-CN.md#manifest-v2扩展)。
2. 实现 Sidecar。[Go SDK](../sdk/go/README.md)(仅依赖标准库)已经处理传输、
握手、序号、content ref 与关闭;语言无关的参考见
[线协议](./EXTENSION_PROTOCOL.zh-CN.md)和
[生成方法索引](./EXTENSION_PROTOCOL.generated.md)。
3. 构建 Runtime 二进制,先用
`reasonix plugin install /path/to/plugin --dry-run` 检查信任与能力,再用
`--link --yes` 安装。
4. 用 `reasonix plugin doctor <name>` 校验,在空闲时运行 `/reload`,然后验证
插件贡献的拦截器、Provider、UI action 或资源。
SDK 使用不可变的 `sdk/go/vX.Y.Z` 标签发布,首个公开版本为
`sdk/go/v1.0.0`。该标签存在之前,请从源码 checkout 使用 starter,不要依赖
未版本化的 module API。
## 兼容性
- 原生 `reasonix-plugin.json` 必须声明精确版本
`reasonix.io/plugin/v2`。扩展 Manifest v1 从未公开发布,因此不提供
v1 双读或自动迁移路径。
- 旧版本 Reasonix 会忽略扩展专有状态:会话级
`<session>.extensions.json` sidecar 文件、`plugin/...` 模型 ref
(仅报告模型不可用),以及 `extension_surface`/`extension_status`
事件类型(旧前端丢弃未知类型;未声明 `reasonix.extensionSurface`
的 ACP 客户端收到文本 fallback)。
- `plugin-packages.json` 保持现有 schema;已启用的已安装 Runtime 即
为信任记录。
## 安全模型
代码型扩展运行在 Reasonix Sandbox 之外,继承未过滤的完整环境:可以
读取完整会话与环境、绕过权限与工作区限制、直接操作本机;它在
`permission.decision` 上的 "allow" 可覆盖宿主 deny。作为约束,宿主
保证:
- 只有通过插件安装流程的插件才能启动 Runtime——项目配置永远无法
声明代码型 Sidecar;
- 握手时拒绝任何超出 Manifest 声明的能力;
- 所有替换都按点位 DTO 与 Schema 重新校验;
- Sidecar 的诊断输出、结构化 UI、拦截器原因和 Provider 错误在进入 UI、
日志或错误界面前由宿主进行凭据脱敏;普通 Provider/模型内容作为产品
数据保持原样;
- Sidecar 崩溃只令其自身操作明确失败——Reasonix 绝不静默回退到
其他模型或策略。
-131
View File
@@ -1,131 +0,0 @@
# Reasonix Extension Protocol v2
Extension Protocol 是 Reasonix(**宿主**)与以独立进程运行的代码型扩展
(**Sidecar**)之间的稳定线上契约。安装了带有 `runtime` 块的插件后,
它通过该协议拦截运行时事件、持有替换策略、提供流式模型 Provider、发布
结构化 UI——全程不链接进宿主二进制。
- 协议 ID:`reasonix.extension.v2`
- 机器可读 Schema:`internal/extension/protocol/schema.generated.json`
- 方法/事件/限额/错误索引:`docs/EXTENSION_PROTOCOL.generated.md`
(生成产物,CI 防漂移校验)
- Go SDK(已实现下文全部内容):`sdk/go`
本文档是生成索引的文字说明。两者不一致时,以生成 Schema 为准。
## 传输
- 严格 JSON-RPC 2.0 over **NDJSON**:stdin/stdout 上每行一个完整 JSON
对象。stderr 归扩展用于诊断输出,宿主只保留限长、经凭据脱敏的尾部
用于错误展示。
- 双向单帧上限 **8 MiB**;超帧是连接级致命错误 `frame_too_large`。
- 请求 ID 为整数;`params` 必须是对象。帧层容忍未知成员,但 DTO 解码
是严格的(拒绝未知字段),拼写错误会立即暴露。
## 生命周期
1. 宿主以 exec form 启动 Sidecar(不经 Shell),并首先发送
`extension/initialize`,参数携带 Manifest 期望:宿主将接受的
intercepts、replaces、providers 和 UI actions。同一 runtime generation
最多并行初始化 4 个 Sidecar,并共享 30 秒总启动预算。
2. Sidecar 应答自身声明。宿主校验:协议 major version 精确匹配,且
subscriptions、替换槽、Provider、UI action 都必须是 **Manifest 声明
的子集**;超出部分以 `capability_not_declared` 握手失败。
3. 宿主发送 `extension/initialized`。此前任何 Sidecar→宿主流量都会
使连接失效。
4. 关闭是有界的:`extension/shutdown`(带超时)→ 关闭 stdin →
未退出则终止进程树。
5. 崩溃:Sidecar 死亡会取消其全部 pending RPC。若它持有当前选用的
Provider 或替换槽,当前操作明确失败——宿主绝不静默回退到另一个
模型或策略。崩溃的 Sidecar 只在空闲 reload 时重启。
## Content ref
被标记为可外置且超过 **64 KiB** 的载荷字段会卸入宿主 content store:
帧内携带 `ExternalizedField` 描述符(JSON pointer、content ref、字节
数、SHA-256)与 `null` 占位。对端通过 `host/content/read` 以
**256 KiB** 分页取回字节,并校验字节数与哈希。单个 content 对象上限
**8 MiB**。未知或过期 ref 报 `content_ref_expired`。
## 拦截
十七个冻结 hook 点(见生成索引)。`extension/intercept` 是阻塞式;
`extension/event` 是对相同点位的只读观察。事件通过有界、非阻塞的写入
队列投递;队列饱和时丢弃该观察并告警,不会卡住 Agent。
- 普通 Interceptor 按确定顺序**串行**执行:priority 升序(Manifest
`priority`,-1000..1000,默认 0),其次插件 ID,再其次注册序号。
- 每次调用的裁决:`continue`(透传载荷)、`block`(中止操作并给出
用户可见原因)、`replace`(替换载荷——宿主在使用前按点位 DTO 与
Schema 重新校验)、`allow`/`deny`(仅在 `permission.decision` 合法)。
full-trust 的 `allow` 可覆盖宿主 deny,并会记录审计。
- 替换**策略槽**(`system_prompt`、`context`、`provider_request`、
`provider_response`、`compaction`、`session_policy`、`permission`、
`frontend_events`、`tool:<name>`、`provider:<ref>`)在所有已安装
插件中只有一个 owner。链式拦截先执行,槽主做最终裁决;槽主的超时
或错误总是令当前操作失败。
- 超时:输入/工具/权限点位默认 5 秒;会话/上下文/压缩/系统提示词族
默认 30 秒;Manifest 可调,上限 60 秒。可选观察型扩展超时只告警
一次并跳过;required 扩展与槽主超时则令操作失败。
## 流式 Provider
具备 `providers` 能力的扩展应答 `extension/provider/catalog`,返回与
宿主 Provider 等价的描述符(模型、上下文窗口、价格、视觉、推理、
effort)——绝不包含凭据。模型以 `plugin/<plugin>/<provider>/<model>`
形式出现。
流式调用遵循 `extension/provider/stream/open` → `stream/chunk` →
`stream/end`:
- chunk 使用从 1 开始的连续序号;`stream/end.lastSeq` 冻结结束边界。
宿主缓冲乱序 chunk、丢弃重复,缺 chunk 持续存在时以 interrupted
失败并指明缺失序号。
- chunk 类型:`text`、`reasoning`(含 `signature`)、
`tool_call_start`、`tool_call_args_delta`、`tool_call`、`usage`
(含 cache tokens)、`done`、`error`。Provider 错误必须由产生方脱敏,
宿主还会进行防御性二次脱敏。
- 取消流上下文会发送 `stream/cancel`,Sidecar 必须停止产生 chunk。
- 扩展自行读取所需环境与凭据;宿主绝不把其他 Provider 的 API key 或
header 发送给它。被选 Provider 崩溃时不自动回退到其他模型。
## 结构化 UI
具备 `ui` 能力的扩展可发布 `status`、`card`、`form`、`notification`
载荷(`host/ui/publish`),也可发起提问(`host/ui/request`:confirm、
input、select、multiselect)。UI 表面**只有结构化数据**:不允许
HTML、CSS、JavaScript、远程脚本、任意前端组件或未受控 URL;Markdown
走各前端现有的安全渲染器。每次表面更新都携带插件 ID、surface ID、
session ID 与运行时 generation;旧 generation 的更新会被丢弃,tab
切换或 reload 后的迟到结果无法覆盖新状态。
初始化时声明的 action 以 `/<plugin>:<action>` 命名空间暴露,通过
`extension/ui/action` 调用;表单提交经 `extension/ui/submit` 到达扩展。
## 错误
域错误使用 JSON-RPC 错误码 `-32000` 并携带结构化数据(reason、
retryable、action);`protocol_error`、`unknown_method`、
`invalid_params`、`internal` 使用标准 JSON-RPC 错误码。冻结的 reason
表见生成索引。
## 稳定性契约
major v2 内只允许:新增 optional 字段、新增枚举值、新增方法。既有
必填字段、方向、限额、错误 reason 与语义永不改变。canonical Schema
及其 SHA-256 hash 由 `cmd/extension-protocol-gen` 产生;CI 的
`go test ./...` 会运行确定性生成测试(
`TestGeneratedArtifactsAreDeterministicAndCommitted`),任何漂移——包括
意外语义变更——都会令构建失败。
## 安全模型
代码型扩展是**完全信任(full trust)**的:它运行在 Reasonix Sandbox
之外,继承未过滤的完整环境,可读取完整会话与环境、绕过权限、直接
操作本机。安装、更新、替换或 `--link` 一个带 `runtime` 块的插件即
代表授权——没有二次确认。只有通过插件安装流程(写入
`plugin-packages.json`)的插件才能启动 Sidecar;项目配置永远无法
声明。Sidecar 的诊断输出、结构化 UI、拦截器原因和 Provider 错误在
进入 UI、日志或错误界面前都会经过宿主的凭据脱敏;普通 Provider/模型
内容作为产品数据保持原样。安装预览、插件详情与能力诊断对 runtime
插件始终展示 FULL TRUST 区块。
-112
View File
@@ -1,112 +0,0 @@
# Extension Runtime v2(时空可组合性)
Reasonix 插件/运行时 v2 模型说明。英文版:[EXTENSION_RUNTIME_V2.md](./EXTENSION_RUNTIME_V2.md)。
## 协议与 Manifest
- 插件清单:仅接受 `apiVersion: reasonix.io/plugin/v2`(拒绝 v1 与无 apiVersion 的 native)。
- 兼容边界:不提供 v1 双读或自动迁移。扩展 Manifest v1 从未公开发布,
因此 v2 是首个受支持的 Runtime Manifest。
- 扩展线协议:`reasonix.extension.v2`(major 2)。
- Handshake 的 `provides` 不得超过 manifest 的 provides 上限。
## 运行时模型
```text
RuntimeSnapshot = 不可变配置与依赖视图
RuntimeSet / EffectScope = 一代 live 资源
RuntimePlan = 旧 → 新 的转移(子图分类)
RuntimeOwner = 单个 session lineage 的 gate + receipt + stream/file 证据
Controller = 已发布 generation 的消费者(admission 绑定 RuntimeOwner)
```
组件状态:`Inactive → Preparing → Active → Draining → Inactive`(或 `Failed`)。
## 重载
- 优先 `boot.RebuildFrom(previousBuildResult, opts)`。
- no-op / interceptor / UI / provider / MCP-only 走 **真子图 patch**(不进 `BuildRuntime`);`ReusedController` 时调用方不得 `Close` 旧指针。
- Provider/MCP 子图:`WithLiveContributions` 刷新 interceptor/provider/UI 目录;**system prompt + tool schemas + CacheHash 保持稳定**(只滚 backend)。工具 schema 改名仍需全量 rebuild。
- 窄路径为 **stage → ready → commit**(失败原子):
- **Stage**:启动/收养 sidecar,构建下一代 dispatcher/resolver;**不**安装 stream router,也**不**对 UI hub 调用 `BindGeneration`。
- **Ready**:等待 sidecar 就绪。
- **Commit**:安装 stream router、绑定 UI generation、替换 controller 绑定,再 publish。
- stage/ready/commit 任一步失败:`RollbackPlanStart` 回挂 Unchanged 客户端;stage 前的 stream router 继续消费 `stream/chunk` / `stream/end`。
- **Stage 期间的 UI**:在 commit 之前仍绑定旧 generation。sidecar 在 handshake/ready 期间若用**下一代** generation 发 `host/ui/publish` 或 `host/ui/request`,会被当作 stale **丢弃**。协议约定:在 runtime generation 发布前,不要依赖 UI 可见性。
- 迁移成功后:**发布** 新 generation,再 **排空** 旧 sidecar。
- Drain 超时先 cancel 已注册工作(被替换的 controller、**host provider stream**(`HostStreamRegistry`)、extension provider stream、StableProxy/MCP 在途),再写 `drain-timeout` receipt。
- Draining 中的 Controller 拒绝新 turn(`turnDroppedDraining`)。
## 诊断
```bash
reasonix doctor runtime
reasonix doctor runtime --json
reasonix plugin doctor <name>
```
输出组件状态、计划、effect receipt、可恢复性、lifecycle metrics,以及进程内的
`runtimeOwnerFallbacks` 计数。产品启动路径会绑定独立 owner;该值非零表示仍有
兼容路径落到了共享默认 owner,需要补齐显式接线。
计划诊断拆分两个事实:`prefixChanged` 在构建完成后比较新旧 snapshot 的
`CacheHash` 得出;`providerChanged` 表示 Provider capability 的新增、删除或
重载。因此仅滚动 Provider backend 且 system prompt、tool schemas 字节不变时,
诊断为 `prefixChanged=false, providerChanged=true`。
## Effect receipt
不可逆外部动作记入当前 `RuntimeOwner` 的 receipt store。独立 session
不共享 publish/drain 状态或恢复证据。Recovery **不得** 对 irreversible
声称 rollback 成功;使用 owner 级的 `AssessRecoverability(generation)` /
`DecideResume(generation)`。
Receipt ledger **只支持进程内 rebuild/resume**,不会持久化;进程崩溃后的恢复
不在本次 Runtime v2 的范围内。内存最多保留最近 32 个 generation、每代 256 条
receipt。淘汰时也会释放关联的文件 prior 字节,并将该代证据标记为已截断;证据
不完整时,诊断不会声称 clean rollback。
消息发送去重与 receipt 使用同一保留周期:消息 receipt 被淘汰时会同步释放对应的
`(generation, messageID)` 键,避免形成第二份无界账本。文件 prior 每条最多 8 MiB、
每个 `RuntimeOwner` 合计最多 32 MiB;超过任一上限会记录 `prior_truncated`,恢复
判断按保守策略处理,不会声称 clean rollback。
- Provider stream open 会记录 `provider-submit:<id>`(不可逆)。
- 已完成的 Provider stream 会注销 drain 回调;generation gate 只保留仍在途的 stream。
- Drain 超时 force-expire 记录 `drain-timeout:<gen>`。`ScheduleDrainWatch` 仅在确有
generation 正在 drain 时启动,并将快速连续 publish 合并为每个 owner 一个 watcher;
doctor sweep 仍作为兜底。
- 每个 owner 最多保留最近 256 个 late-cancel 过期 generation 标记。
## EffectScope 归属
一代运行时的 live 资源挂在 `RuntimeSet` / `EffectScope`:
| 资源 | 跟踪方式 |
| --- | --- |
| Sidecar manager | Activator 中 Cancelable effect |
| UI hub 绑定 | `TrackUIHub` |
| MCP plugin host | 清单 + `session-resources` dispose |
| LSP manager | `TrackWatcher` 清单 |
| Session cleanup 链 | `TrackControllerCleanup` |
| Provider 已提交 | `RecordProviderSubmit` receipt |
## 验收对照
| 规格验收项 | 状态 |
| --- | --- |
| 每个资源与 session lineage 有明确 owner | 完成(`RuntimeOwner` + sidecar/MCP/UI/LSP/stream/file receipt 接线) |
| 激活失败不泄漏 / 不 publish | 完成 |
| 缺依赖 → Inactive 诊断 | 完成(结构化 missing requirement + Unavailable) |
| 真子图 rebuild(不进完整 BuildRuntime) | 完成(None/Interceptor/UI/Provider/MCP) |
| Publish/drain 顺序 + drain 取消 | 完成 |
| 不可逆永不标 rollback 成功 | 完成(recovery `AssessRuntimeResume`) |
| 原生 Runtime Manifest 严格 v2-only | 完成(无 v1 双读 / 自动迁移) |
| 缓存稳定性守卫 | 完成 |
| Doctor 解释 inactive + resume | 完成(CLI + desktop RuntimeDoctor UI) |
| 无外部运行时依赖 | 完成 |
## Phase 5 决策
- **兼容性**:v2 是首个受支持的扩展 Runtime Manifest;install/doctor/boot
不双读 v1,也不自动迁移。
- **性能**:见 [EXTENSION_RUNTIME_V2_PERF.zh-CN.md](./EXTENSION_RUNTIME_V2_PERF.zh-CN.md)。
- **README / SDK**:示例 manifest 在 `sdk/go/examples/*`;wire 类型以 protocol gen 为准。
-49
View File
@@ -1,49 +0,0 @@
# 扩展运行时 v2 — 性能基线
软 CI 阈值见 `internal/extension/bench_threshold_test.go`。
Benchmark 见 `internal/extension/benchmark_test.go`。
## 目标(开发机 / CI 软失败)
| 操作 | N | 软上限 |
| --- | --- | --- |
| `BuildDependencyGraph` | 32 组件 | < 50ms |
| `DiffRuntimePlan` no-op | 同图 | < 20ms |
| `EffectScope.Dispose` | 64 effects | < 50ms |
## 增量 vs 全量 rebuild
- **no-op / interceptor / UI / provider / MCP-only**(`RebuildFrom` + 真子图 patch):**不得**调用 `BuildRuntime`。指标:`NoOpRebuilds` / `SubgraphRebuilds`。
- **全量**(`SubgraphSidecar` / `SubgraphFull`):`FullRebuilds` + 完整 `BuildRuntime`。
测量命令:
```bash
go test ./internal/extension/ -run 'TestGraphAndPlanLatencyBaseline|TestEffectScopeDisposeBaseline' -count=1
go test ./internal/extension/ -bench 'BenchmarkDependencyGraphAndPlan|BenchmarkExtensionKernelStartup' -benchmem -count=3
go test ./internal/boot/ -run 'TestIntegrationNoOpDoesNotBuildNewController|TestRebuildFromNoOp' -count=1
```
## 缓存命中
no-op、UI/interceptor-only 以及只滚动 backend 的 Provider/MCP 计划会保持
system prompt、tool schemas 与 `CacheHash` 字节稳定。Provider capability 变化通过
`providerChanged` 呈现,不会误报 `prefixChanged`;MCP schema 新增、删除或改名则
归类为全量 rebuild,并有意重新计算 prefix。保留 `ReuseAssembly` 时仍可跳过
skill/command/hook rediscovery。
## Sidecar 启动 / drain
`StartPackagesWithPlan` 收养 Unchanged 客户端;仅启动 Added/Reloaded。
Publish 后 `DrainPlan`;默认 drain TTL 30s,超时先 fire cancel 再写
`drain-timeout-<gen>` receipt。
冷启动 publish 不创建 watcher;存在 drain 时,快速连续 publish 在每个 runtime
owner 上共用一个定时 watcher,并只等待最早 drain 的剩余 TTL。每个 owner 的
过期 generation 标记最多保留 256 个。
Receipt 证据仅存在于当前进程,最多保留 32 个 generation、每代 256 条。发生
淘汰时按保守策略处理:禁止声称 clean rollback,而不是隐藏证据缺失。
消息去重键会随对应 receipt 淘汰而释放。文件 prior 每次写入最多保留 8 MiB,
每个 runtime owner 合计最多 32 MiB;超限 prior 不保留,并阻止 clean rollback 判断。
已完成的 Provider stream 会立即移除 drain 回调,长寿命 generation 只保留活跃 stream
的取消状态。
-195
View File
@@ -1,195 +0,0 @@
# Goal 模式 — 连续执行、结构化完成协议与 Delivery 职责拆分
Reasonix 的 Goal 模式(`/goal`)将目标推进(Goal)、验收(Delivery)和权限(Ask/Auto/Yolo、Sandbox)三者保持正交:Goal 是唯一的跨 turn 调度器,Delivery 是纯质量门禁,工具权限与沙箱不受 Goal 开关影响。
## 功能一览
| 功能 | 触发方式 | 效果 |
|------|----------|------|
| 结构化完成协议 | `update_goal` 工具 | 每轮目标 turn 结束时模型通过工具报告 continue/complete/blocked(含 reason 与 next_action),取代旧的 `[goal:*]` footer 文本标记 |
| 完成校验 | 默认 | `complete` 声明必须通过 Delivery readiness(todos、验证、review、签收、能力门禁)才会真正完成;不满足时用缺失项开启下一轮 |
| 完成自述与对账 | `update_goal` 的 `completion` | `complete` 可附带自述:`verified` 命令逐条与本会话真实 receipt 对账,没跑过 / 跑失败 / 早于最后一次改动都记为 unbacked claim;`unverified` 与 `risks` 是宿主推断不出的声明,只增不减,永远不阻塞完成 |
| 独立评审 | 无报告时 | 模型未调用 `update_goal` 时,宿主调用一次独立 bounded evaluator 判定;评审不可用/出错/不确定时安全暂停,绝不默认继续 |
| 连续执行 | 默认 | 不设默认 model rounds、Goal turns、墙钟时长或数字式卡死上限;相同宿主失败、零新增证据和 Todo 停滞只触发重新规划,不暂停 Goal |
| 显式预算 | `[agent].goal_token_budget` / `--max-steps` / 正数时间或成本预算 | 用户可选的边界耗尽后执行一次无工具总结并暂停当前执行;Goal 与进度保留,可继续。token 预算默认 `0`(关闭) |
| 暂停/恢复 | `/goal pause` / `/goal resume` | 暂停保留 Goal、todo、Delivery checkpoint 与累计运行历史;恢复 `budget_spend` 时授予新的显式预算切片,但不清零累计统计 |
| 立即阻塞 | `blocked` 报告 | 单个 blocked 报告立即结束目标,不再重复三轮确认 |
| 并行调度 | `parallel_tasks` 工具 | 并发派发多个子 agent,各自独立显示结果 |
## 使用方式
### 默认模式
```bash
/goal 实现一个 CLI 计算器
```
模型在每个目标 turn 结束时调用 `update_goal`:
- `continue`(附 `reason` 与可选 `next_action`)— 继续推进;
- `complete`(仅在请求完成、输出格式与约束满足、验证已尝试或声明不可用时)— 宿主会用 Delivery readiness 校验该声明;
- `blocked`(仅当下一步需要用户独有信息、不可逆或对外可见操作、或范围变化时)— 立即停止。
`complete` 还可以附一份自述 `completion`:
```json
{"status":"complete","completion":{
"verified":["go test ./..."],
"unverified":["desktop UI 未实际操作验证"],
"risks":["迁移不可逆"]
}}
```
宿主对这份自述只做一件事——**对账**。`verified` 里的每条命令都会去本会话的真实 receipt 里找:没跑过、跑失败、或者最后一次运行早于最后一次改动,都会被记成一条 unbacked claim。`unverified` 与 `risks` 宿主无从推断,因此原样保留;它们**只增不减**,一份自述永远不能抹掉宿主自己发现的缺口,也永远不会因为诚实申报而阻塞完成。
`update_goal` 只在活动 Goal turn 中可用;普通聊天调用会收到结构化错误且不改变任何状态。同值重复调用幂等,`continue` 可升级为 `complete`/`blocked`,终态后冲突调用被拒绝;目标被替换或清除后,迟到的报告/用量一律按 scope+epoch 拒绝。
### 统计、显式预算与暂停
旧的 simple/write/research 类别和 `/goal --simple`、`--research` 参数只为 sidecar/CLI 兼容保留,
不再改变执行额度。executor、planner、subagent、compaction、router、reviewer、evaluator 等计费用量
累计到 `tokensUsed`,真实 HTTP 请求(含重试)累计到 `requestsUsed`,Goal Run 的实际工作时间累计到
`workDurationMs`。默认情况下这些字段与 `turnsUsed` 都只做统计:
- 未配置 `goal_token_budget` 时 `tokensLimit` 为 `0`;配置正数后只表示用户选择的累计 token 阈值;
- 没有 provider 请求前的 token 预留/准入;
- 未配置对应预算时,累计 turn/token/request/work time 再大也不会单独暂停 Goal;
- `turnsLimit`、`noProgressLimit`、`budgetExtensions` 继续对外保留为 deprecated 兼容字段,固定返回 `0`。
可停止连续执行的条件:完成、模型通过 `update_goal(blocked)` 报告真实用户/外部阻塞、evaluator
故障或不确定、用户主动 pause/stop/clear、Provider/权限/宿主不可恢复错误,以及用户显式设置的
正数 token/步数/时间/成本预算。`task_time_budget_minutes = 0`(以及兼容读取的负数)关闭时间边界,
只有正数才启用。结构化卡死检测、Todo stall 与 `noProgressTurns` 只注入策略纠偏,不改变 Goal 状态。
**轮数不再是任何停止条件。** `/goal status` 在未设置 token 预算时显示纯统计:
```
runtime: turns 57 · requests 143 · tokens 2800000 · work time 42m
```
配置 `goal_token_budget` 时 token 统计会显示当前显式阈值;`/goal resume` 从 `budget_spend` 暂停
恢复时授予一个新的完整预算切片,但 turns、tokens、requests 与 work time 继续累计。旧版本因 `budget_turns`、`budget_tokens`、
`goal_run_budget`、`goal_stuck` 或 `no_progress` 暂停的 sidecar 在加载时自动改为 `running` 并原子持久化,
但加载本身不会发送模型请求。活动 Goal 在磁盘写 `turnsLimit: -1` 作为旧 reader 的无限制哨兵;
新 API 将其解释为 `0`。新的 `budget_spend`(用户显式预算)不会被自动迁移;manual pause、evaluator
failure、legacy archive block 和真实 blocker 同样不自动解锁。
上下文压缩继续使用全局既有策略:仅由 `compact_ratio`(默认 85%)触发一次内容驱动摘要 checkpoint,不另设 soft/snip/force 多阈值。Goal 开启本身不额外触发 summarizer,也不改变工具 Schema 或稳定 prompt 前缀。
### 任务合约
复杂目标可以直接写成 Context / Request / Output format / Constraints /
Pause policy。Goal 模式会把这些段落当作执行边界:满足请求、输出格式、约束和必要验证后才结束;
除非下一步涉及不可逆或对外可见操作、范围变化,或必须由用户提供信息,否则继续采用合理默认值推进。
### 并行子任务
```bash
/goal 研究 Go 的三个标准库并写示例
```
Agent 可以调用 `parallel_tasks` 工具同时派发多个独立子任务:
```
parallel_tasks(tasks=[
{prompt: "研究 encoding/json,写示例", description: "json research"},
{prompt: "研究 net/http,写示例", description: "http research"},
{prompt: "研究 sync,写示例", description: "sync research"},
])
```
每个子任务在独立 goroutine 中运行,工具调用会嵌套显示为独立卡片,结果聚合返回。
### 任务依赖
如果子任务之间有依赖关系,可以用 `depends_on` 指定:
```
parallel_tasks(tasks=[
{prompt: "写一个加法函数到 add.py", description: "add"},
{prompt: "写一个乘法函数到 mul.py", description: "mul"},
{prompt: "在 main.py 中调用 add 和 mul", description: "main", depends_on: [0, 1]},
])
```
独立任务(add、mul)先并发执行;main 等前两个完成后再启动。
## Prometheus 规划面试
在写代码前,先让 AI 帮你理清需求:
```
/prometheus 重构用户认证模块,改成 JWT
```
Prometheus 会逐个问澄清问题:
```
1. 用户模块当前是 session 还是 token 认证?
2. 需要支持 refresh token 吗?
3. 现有用户表结构是什么样的?
```
回答完问题后,Prometheus 自动生成可执行的计划。然后你可以用 `/plan-exec` 来执行。
## 实现细节
### 每轮决策顺序
1. 运行工作模型;
2. 获取结构化 Delivery readiness;
3. 读取本轮的 `update_goal` 报告;
4. 没有报告时调用一次独立 evaluator(readiness 已明确缺失项时直接继续,不调用);
5. 应用 readiness 与 evaluator fail-closed 结果;
6. 由 Goal FSM 独占决定 complete、continue、blocked 或 pause。
`complete` 只有在 readiness 通过时才被接受;`blocked` 立即停止;evaluator 超时、报错、JSON 非法或返回 `uncertain` 一律安全暂停。
### 证据审计门控(Delivery)
Delivery 收敛为纯 readiness 服务,宿主可消费的结构化结果为
`ReadinessResult{Ready, Missing, Reason, ProgressKey}`:
- Canonical todos(当前 todo 列表)
- Project checks(来自 AGENTS.md 的 verify 指令)
- Delivery 专属验收项(mutation、verification、review、complete_step 签收、capability 门禁)
Delivery 不再自行注入隐藏模型消息做 3/6 次 readiness 重试:普通 Delivery 回合在第一次未满足的最终回答后立即结束并显示恢复卡;Goal + Delivery 回合由 Goal FSM 自动续轮,不显示需要用户点击的重复卡片。
### 进展签名
只有宿主可验证且对当前 Goal **新颖**的信息才能重置停滞计数:新的读取/搜索结果、todo 状态变化、新的有效 mutation/verification/review/signoff receipt、Delivery checkpoint 变化、终态 `update_goal` 报告。读取和查询由规范化工具名、参数及宿主观测到的结果摘要标识;完全相同的重复调用、仅改变措辞的回答或重复 continue 理由都不能伪造进展。证据摘要以有界窗口持久化,不保存工具输出正文。
### Todo 状态流
```
todo_write → agent 创建任务列表
complete_step → agent 标记某一步完成
advanceGoalAfterTurn → 读取 update_goal 报告 + readiness + evaluator
├─ complete + readiness 通过 → 完成
├─ complete + readiness 缺失 → 拦截并列出缺失项,继续循环
├─ blocked → 立即阻塞
├─ 无报告 → evaluator 判定一次(失败则安全暂停)
└─ 数字式无进展/Todo 阈值 → 重新规划并继续(不改变 Goal 状态)
```
### 并行调度架构
```
parallel_tasks Execute()
├─ 对每个子任务:
│ ├─ 发射 ToolDispatch 事件(前端渲染卡片)
│ ├─ 创建嵌套 sink(subSinkFor)
│ ├─ 启动 goroutine 运行 RunSubAgentWithSession
│ └─ 子任务工具调用自动嵌套显示
├─ WaitGroup 等待全部完成
└─ 聚合结果返回
```
## 相关代码
- `internal/control/goal.go` — Goal FSM、turn recorder、兼容迁移、暂停/恢复与运行统计
- `internal/control/turn_orchestrator.go` — 每轮决策流程、evaluator 调用
- `internal/control/input.go` — `/goal` 命令解析与任务合约注入
- `internal/goaleval/` — 独立 bounded evaluator
- `internal/tool/builtin/updategoal.go` — `update_goal` 工具
- `internal/boot/boot.go` — 工具注册与 evaluator 装配
-34
View File
@@ -1,34 +0,0 @@
# 历史搜索 Catalog
Reasonix 将历史搜索投影保存到 `<cache root>/history-search/v1.sqlite`。
Session JSONL、event log、metadata、子 Agent transcript 和 archive 仍是唯一权威
数据。删除或重建数据库不会删除会话,旧版 Reasonix 也继续读取原有权威文件。
Catalog 的 FTS5 只保存规范化检索 token,不保存完整消息正文。英文沿用现有小写与
代码符号语义,CJK 沿用重叠 bigram。SQLite 选出最终候选后,snippet 和 `around`
上下文才从权威文件读取。user、assistant、tool input、tool error、tool output 都会索引,
但普通 tool output 仍不属于默认搜索种类。
权威会话提交成功且文件锁释放后,保存路径只发送非阻塞、按 path 合并的索引提示。
连续 append 只读取 display index 的新增范围;rewrite、通知丢失、外部进程写入或指纹
不一致会在后台重建单个 source。完整 transcript decoder 始终单并发,checkpoint 按
source 持久化,单个坏会话不会阻断其他历史。
Agent 的 `history` 工具与 Desktop 历史管理器共享该投影。搜索不会同步扫描目录;首次
索引未完成时立即返回已有结果和明确进度。open、running、current 等运行态只从内存
覆盖,SQLite 不能恢复陈旧所有权。面向 provider 的工具名、描述、schema、默认值和
顺序保持逐字节不变。
数据库遵循通用可丢弃投影策略:本地盘使用 WAL、`synchronous=NORMAL`、外键、用户
私有权限和短 busy timeout;远程或不可用缓存自动退化为内存。完整性或迁移失败只隔离
并重建缓存;遇到未来 schema 时保留原库并进入 degraded。
诊断与安全重建命令:
```sh
reasonix doctor catalogs [--json]
reasonix catalogs reindex history [--dir PATH ...] [--json]
```
诊断不会输出 query、token、snippet、消息、tool arguments 或 provider 内容;reindex
只替换该可丢弃投影。
-138
View File
@@ -1,138 +0,0 @@
# 迁移到 Reasonix 1.0(Go 重写版)
<a href="./MIGRATING.md">English</a>
Reasonix 1.0 是一次从零开始的 **Go 重写**。它使用全新的代码库,并不是 `0.x` TypeScript 版本的增量升级。本文说明两个版本的差异以及迁移方法。
## 摘要
| | 旧版(v1) | Reasonix 1.0+(v2) |
| --- | --- | --- |
| 语言 | TypeScript / Node.js | Go |
| 分支 | [`v1`](https://github.com/esengine/DeepSeek-Reasonix/tree/v1)(仅维护) | `main-v2`(默认、活跃开发) |
| 版本 | `0.x`(最高 v0.54.x) | `1.0.0`+ |
| 安装 | `npm i -g [email protected]`(固定到某个 `0.x` 版本) | `npm i -g reasonix`;也可使用 release 归档或源码构建 |
| 代码智能 | embedding 语义搜索 + tree-sitter 符号索引 | LSP 辅助代码读取,以及 grep/read_file/glob;语义索引尚未移植 |
“v1”和“v2”表示代码库代际,而不是 semver 主版本:v1 从未发布 1.0,因此 Go 重写版使用 `1.x` 版本号。
## 安装 1.0
`npm` 仍是主要安装渠道。npm 包会下载预编译的 Go 二进制文件,方式与 esbuild/biome 类似;二进制本身是独立的 Go 可执行文件,npm 不是运行时依赖。
**`npm i -g reasonix` 会安装当前正式的 `1.x` 版本。** npm 的 `latest` 标签已从 `1.17.5` 起切换到 Go 版本。以后不再公开发布候选版本;旧 `next` 与 `canary` 标签仅作为兼容别名,始终指向同一个正式版本。旧版 `0.x` 仍可通过固定版本安装:
```sh
npm i -g reasonix # 当前正式的 1.x
npm i -g [email protected] # 固定到旧版 TypeScript 构建
```
每个 GitHub release 都附带预编译归档(`reasonix-<os>-<arch>.tar.gz` / `.zip`)和桌面安装包。它们与 npm 是不同的安装渠道:桌面安装包不会改动通过 `npm i -g` 安装的 CLI,因此 shell 中的 npm `0.53` 与 `1.x` 桌面应用可以共存,并不冲突。
也可以从源码构建:
```sh
git clone https://github.com/esengine/DeepSeek-Reasonix # 默认分支 main-v2(Go)
cd DeepSeek-Reasonix && make build # -> bin/reasonix(.exe)
```
## 配置
| 旧版 | Reasonix 1.0 |
| --- | --- |
| TypeScript 配置文件 | 项目使用 `reasonix.toml`;从 v1.8.1 起,全局配置为 Reasonix home 下的 `config.toml`(macOS/Linux:`~/.reasonix/`;Windows:`%AppData%\reasonix\`)。参见 `reasonix.example.toml` 和[配置路径](./CONFIG_PATHS.zh-CN.md) |
| 环境变量 / API key | provider 配置保留 `api_key_env`;保存的 key 位于 Reasonix home 的 `.env`(`DEEPSEEK_API_KEY`、`MIMO_API_KEY` 等) |
| 项目记忆 | `REASONIX.md`(含自动记忆),兼容 Claude Code |
| MCP server | 在 `reasonix.toml` 中使用 `[[plugins]]`,或直接读取 Claude Code 的 `.mcp.json` |
首次启动时,v1.8.1+ 会执行一次非破坏性导入。它会读取以下旧配置:
- `~/Library/Application Support/reasonix/config.toml`
- `~/.config/reasonix/config.toml`
- `~/.reasonix/reasonix.toml`
- v0.x 的 `~/.reasonix/config.json`
导入内容包括 API key、base URL、语言和 MCP server;缺失的旧凭据会迁移到 `<Reasonix home>/.env`,旧会话也会从历史目录导入。原文件不会被修改,Reasonix 会在导入后显示启动提示。
会话会根据 v0.x sidecar 元数据回到原工作区,并沿用旧摘要作为标题;工作区已不存在的会话会进入全局会话目录。可通过 `--resume` 或历史面板恢复这些会话。自动配置导入仅在尚未存在 v1.8.1+ 配置时运行;若新配置已经生成,请手动补入缺失值。
如果首次启动时旧路径尚不可用,可在交互式会话中运行 `/migrate`。若看到 `unknown command`,请先升级到包含该命令的 Go 版本。该命令会扫描旧配置、凭据、记忆和会话,并仅导入尚未导入的内容;它不会覆盖已有 `config.toml` 或记忆文件,也不会绕过会话导入标记。
若旧 v0.x 会话位于自定义 Windows 目录,可指定来源:
```text
/migrate --from "D:\OldReasonix"
```
完整路径和限制见[配置路径](./CONFIG_PATHS.zh-CN.md)。
## Context Engine v2 升级
指令与记忆升级会自动完成,不需要 setup mode、re-index 命令或新配置:
| 现有数据 | 升级行为 |
| --- | --- |
| `REASONIX.md`、`AGENTS.md`、`CLAUDE.md` | 作为常驻指令加载,并附带来源、目录、precedence、imports 和 diagnostics;原文件名继续有效。 |
| 嵌套指令文件 | 从 workspace root 解析到当前目标路径;同一目录内 `.local.md` 优先,更深目录仍高于更浅目录。 |
| 没有 `id` / `revision` 的旧事实 | 获得确定性的 scope-aware `legacy-*` ID,并从 revision 1 开始;migration 幂等。 |
| 没有 `metadata.scope` 的旧事实 | 根据原本拥有该文件的 project/global 目录推导 scope。 |
| 现有 `MEMORY.md` | 作为派生 index,根据 active fact 文件重建;陈旧手写条目不会变成事实。 |
| 现有 active facts | 保持 active,之后发生修改时才开始产生 revision history。 |
| 现有 archive entries | 继续排除在 recall 外,可从 Context Center 或 `/memory recover` 显式恢复。 |
| 旧 Memory v5 transcript | 继续可读;preview 会从 `<memory-compiler-execution>` 恢复原始用户提示。 |
| `[agent].memory_compiler` | 已退役,由既有一次性配置 migration 清除。 |
升级后第一次当前版本启动会补齐缺失的 identity/time metadata,不修改 fact body。若新旧版本
共享同一 state root,兼容路由字段会避免旧客户端把事实移到错误 scope 目录。
升级后请使用诊断命令,不要手工修改 migration state:
```text
/memory
/memory instructions
/memory recall
/memory revisions <id-or-name>
/memory archived
```
新的相关事实会自动召回。只有有界、非敏感、纯创建的 project/reference 事实可以免确认保存;
全局事实、偏好、feedback、更新、重复项、敏感内容和所有归档操作仍是显式用户决定。桌面
Suggestions tab 会自动扫描,但候选在用户接受前绝不会写入。
完整 precedence、freshness、恢复、cache、隐私与远程 workspace 契约见
[Context Engine v2](./SESSION_MEMORY_RETRIEVAL.zh-CN.md)。
## 保持不变的部分
agent 核心延续了原有能力:循环、读写编辑与 glob/grep/bash 等工具、子智能体(`task`、explore/research/review)、Skill、Hook、Plan 模式、MCP 客户端,以及针对 DeepSeek 前缀缓存的设计。
## 主要变化
- **代码智能**:Go 重写版通过 LSP 辅助代码读取,并结合 `grep`、`read_file` 和 `glob` 理解本地代码。v1 的语义搜索与 tree-sitter 符号索引尚未移植,CodeGraph 也不再以内置 MCP server 形式提供。
- **Plan 模式**:新增 `complete_step`,用于基于证据确认步骤完成。
- **MCP 项目身份与 schema 缓存 URL 感知凭据**:userinfo 和 token/api_key/password 等查询值不会进入项目运行身份摘要或 schema 缓存键,因此轮换凭据不会改变项目运行时/缓存身份。用户安装的 server 不计算项目身份摘要;已配置 MCP 不再需要旧的启动或逐工具授权回执。
- **MCP 添加后即可使用**:用户通过桌面端、CLI、全局配置、旧配置导入或主动安装插件包添加的 server 默认可信,全局安装统一写入 `config.toml`。仓库内 `reasonix.toml` / `.mcp.json` 声明保留在项目中,同样无需额外启动确认。同名时项目覆盖全局,项目内部 `reasonix.toml` 高于 `.mcp.json`。打开陌生仓库等同于接受其中可执行的项目配置;启动 Reasonix 前应检查 `.reasonix/settings.json`、`reasonix.toml` 和 `.mcp.json`。如果仓库引发异常的 MCP 或 Hooks 行为,可用安全模式重新启动,在恢复期间禁用这些外部集成。
- **stdio MCP 连接持久化**:writer 调用不再创建新进程,浏览器或会话类 server 的状态可以保留。
- **Plan 与权限策略相互独立**:普通内置工具和 Bash 仍遵循 Ask/Auto/YOLO 与 Sandbox;已安装或代理解析的 MCP 写入/破坏性工具,以及来自未授权 server 的读取工具,在整个规划阶段保持阻止。`complete_step` 等执行阶段工具也要等计划获批后才能使用。
- `plan_mode_read_only_commands` 仍可解析和保存,以兼容旧配置,但不再决定主 Plan 流程能否调用工具。安装或通过项目配置声明 MCP server 后,其非破坏性的 `readOnlyHint` 工具会自动进入 planner 与只读子智能体,不需要逐工具信任配置。
- 使用 `read_only_task` / `read_only_skill` 创建技术上只读的子智能体;普通 `task` / `run_skill` 仍可写入,并受权限与 Sandbox 控制。未声明 `readOnlyHint` 的 MCP 工具仍按 writer 处理。
- `default_tools_approval_mode`、`tools.<raw>.approval_mode` 和 `approvals_reviewer` 已停用,加载时忽略并在下次保存时移除;安装或通过项目配置声明 server 后,其所有工具直接可用。
- **Web Dashboard 仍然可用,桌面端更推荐**:需要浏览器访问时,可运行
`reasonix serve` 启动本地 Web UI;日常可视化使用优先选择 Wails 桌面端,
终端工作流继续使用 CLI/TUI。
- 一些细粒度 v1 工具被合并,例如文件管理操作改由 `bash` 完成;少数工具尚未移植,进度在 Discussions 中跟踪。
## 文件编码
Reasonix 1.0 支持读取和编辑 UTF-8、UTF-8 BOM、UTF-16 LE/BE 与 GB18030(GBK 的超集),与 v1 行为一致。
- `read_file` 会把受支持编码解码为 UTF-8 后提供给模型。
- `edit_file` 和 `multi_edit` 会保留文件原编码;编辑 GB18030 文件后仍以 GB18030 保存。
- `write_file` 始终写入 UTF-8。
- `grep` 会在匹配前解码,因此正则表达式可用于非 UTF-8 文件。
## 报告问题
Issue 和 PR 按代码线标记:**`v1`** 表示旧 TypeScript 版,**`v2`** 表示 Go 版。请按实际使用版本提交报告。旧 `v1` 线处于维护模式,只接收 bug 修复,不再新增功能。
如有问题,请发起 [Discussion](https://github.com/esengine/DeepSeek-Reasonix/discussions)。
-460
View File
@@ -1,460 +0,0 @@
# Reasonix 插件包
Reasonix 插件包把 skills、hooks、MCP servers、prompts、主题和代码型扩展组织成一个可安装单元。
## CLI 模式
在终端里使用 `reasonix plugin` 安装和管理插件包。插件包当前按全局范围安装,
写入 Reasonix home 目录。
### 通过 CLI 安装
`install` 接收一个来源:
- GitHub 仓库,例如 `git:github.com/obra/superpowers` 或
`https://github.com/obra/superpowers`。
- GitHub 分支或子目录 URL,例如
`https://github.com/owner/repo/tree/main/path/to/plugin`。
- 本地目录,目录内需要包含 `reasonix-plugin.json`、
`.codex-plugin/plugin.json` 或 `.claude-plugin/plugin.json`。
只预览安装计划,不写文件:
```bash
reasonix plugin install git:github.com/obra/superpowers --dry-run
```
确认计划后安装:
```bash
reasonix plugin install git:github.com/obra/superpowers --yes
```
指定安装名称,或覆盖已安装的同名插件:
```bash
reasonix plugin install git:github.com/obra/superpowers --name superpowers --replace --yes
```
以开发模式使用本地目录:
```bash
reasonix plugin install /path/to/plugin --link --replace --yes
```
CLI 安装参数:
- `--dry-run` 只规划和校验安装,不写文件。
- `--yes` 用于确认执行会写文件的安装。
- `--replace` 允许当前来源替换已安装的同名插件。
- `--name <name>` 或 `--name=<name>` 覆盖插件 manifest 里的名称,
作为本次安装名称。
- `--link` 链接本地插件目录,而不是复制到 Reasonix 的插件存储目录。
移动或删除该目录会导致这个链接插件失效。
如果运行 `reasonix plugin install <source>` 时既没有 `--dry-run`,
也没有 `--yes`,CLI 会拒绝写文件,并提示使用其中一个参数重新运行。
安装和移除命令会输出结构化 JSON,来源于桌面端同一套 install-source 后端。
插件状态和内容写入:
```text
~/.reasonix/plugin-packages.json
~/.reasonix/plugins/<name>/
```
### 通过 CLI 管理
列出已安装插件:
```bash
reasonix plugin list
```
查看某个插件的元数据、根目录、来源以及导出的能力数量:
```bash
reasonix plugin show superpowers
```
如果能读取到能力明细,`show` 也会输出具体清单:
- **skills** 会展示建议的 `/<插件名>:<技能名>` 调用方式和描述。
- **commands** 会展示 `/<插件名>:<命令名>` 调用方式、参数提示和描述。
- **hooks** 会展示生命周期事件、matcher、命令或上下文文件。
- **mcpServers** 会展示服务器名称、传输方式和启动目标。
检查 manifest 和 skill roots 是否可读:
```bash
reasonix plugin doctor superpowers
```
工作区级能力总览(skills / hooks / MCP 合并 / 包根目录)见
[能力诊断](./CAPABILITY_DIAGNOSTICS.zh-CN.md):
```bash
reasonix doctor capabilities --json
# 桌面端:设置 → 诊断
# Agent: /reasonix-guide
```
在不卸载的情况下启用或禁用插件:
```bash
reasonix plugin disable superpowers
reasonix plugin enable superpowers
```
移除插件:
```bash
reasonix plugin remove superpowers --yes
```
`remove` 也可以写成 `uninstall`。它需要 `--yes`,
因为会写入状态并删除复制安装的插件内容。如果是链接模式安装的本地插件,
外部源目录会保留。
### 在 CLI 中使用已安装插件
已安装插件不会打开一个独立聊天界面。插件启用后,Reasonix 会把它的能力加载到普通交互会话里:
- 在交互会话里运行 `/plugins` 可以列出已安装插件包。
运行 `/plugins show <name>` 可以在不离开聊天的情况下查看该插件导出的
skills、hooks、MCP servers 和使用提示。
- **Skills** 会出现在 `/skills` 中。可以用 `/<插件名>:<技能名> [args]` 直接调用,
也可以自然描述任务,让 agent 按 description 选择匹配的 skill。
- **Hooks** 会在配置的生命周期事件里自动运行,例如 `SessionStart`、
`UserPromptSubmit`、`PreToolUse` 或 `PostToolUse`。
- **MCP servers** 会进入正常 MCP/工具流程。用户只需要描述任务,
Reasonix 会在相关时调用插件提供的工具。
如果是在另一个终端里安装、启用、禁用或更新插件,而当前已有 `reasonix` 会话正在运行,
建议开启新会话,或重新打开 `/skills` 确认当前会话能看到预期技能。
## 桌面端设置
打开 **设置 -> 插件**,可以不用 CLI 直接安装和管理插件包。
### 安装插件
安装区有两种模式:
- **本地目录**:点击 **选择插件目录**,从磁盘选择一个插件目录。
选中路径会显示在按钮右侧。
- **Git 仓库**:填写 Git 来源,例如 `git:github.com/obra/superpowers`。
**安装名称(可选)** 可覆盖插件 manifest 声明的名称,用于本次安装或覆盖。
选择来源和选项后,再使用操作按钮:
- **预检** 校验来源并展示计划安装动作,不写入文件。
- **安装插件** 按当前来源和选项执行安装。
- **刷新插件** 从磁盘和配置重新读取已安装插件列表。
安装选项:
- **覆盖同名插件** 允许当前来源替换已安装的同名插件。关闭时,同名安装会失败,
而不是覆盖已有内容。
- **开发模式:链接源目录** 只在 **本地目录** 模式出现。它不会复制插件,
而是直接链接所选目录;适合开发或调试插件。移动或删除该目录会导致这个链接插件失效。
对新的 Git 来源或本地插件目录,建议先点 **预检**。
### 管理已安装插件
已安装插件列表会展示每个插件包以及它导出的 skills、hooks 和 MCP servers。
通过应用外编辑插件文件或配置后,可点 **刷新插件** 重新读取。
展开插件行后可以:
- 启用或禁用插件。
- 查看 **使用方法**,了解该插件导出的 skills、hooks 和 MCP servers。
- 使用 **更新** 拉取或刷新具备更新来源的插件。
- 使用 **诊断** 检查插件 manifest,并查看警告或诊断信息。
- 使用 **移除插件**,确认后卸载该插件包。
### 在桌面端使用已安装插件
桌面端设置页和 CLI 使用同一套运行模型:
- 展开已安装插件,可以看到 **使用方法** 区域。
- 在任意桌面会话里输入 `/plugins` 可以列出已安装插件;
输入 `/plugins show <name>` 可以直接从聊天界面查看同一套使用详情。
- Skills 会展示带插件名的直接命令,例如 `/superpowers:writing-plans`;
在会话中也可以通过 `/skills` 浏览。
- 插件命令统一以带插件名的形式展示和调用,例如 `/superpowers:plan`。
- Hooks 和 MCP servers 作为透明能力清单展示。它们不需要单独的“运行”按钮:
启用的 hooks 会自动触发,MCP 工具会通过普通工具调用流程可用。
- 如果当前打开的会话没有反映插件变更,刷新插件列表并开启新会话。
## 原生 Manifest
Reasonix 原生插件在根目录声明 `reasonix-plugin.json`:
```json
{
"name": "example",
"version": "1.0.0",
"description": "Example plugin",
"skills": "skills",
"hooks": {
"SessionStart": [
{
"command": "hooks/session-start",
"args": [],
"description": "Load startup context"
},
{
"command": "printf 'ready' && ./hooks/audit",
"shell": "bash",
"description": "Run a compound shell script"
}
]
},
"mcpServers": {
"helper": {
"command": "bin/helper"
}
}
}
```
相对路径都按插件根目录解析。Reasonix 安装插件时不会执行第三方安装脚本。
插件 Hook 的执行形态是显式的:
- 只要出现 `args`(包括 `"args": []`),就使用 **exec form**。`command`
是可执行文件,每个参数都会按原值直接传递,不经过 Shell 解析或变量展开。
- 未提供 `args` 且提供 `shell` 时,使用 **shell form**。完整 `command`
会原样交给 `bash`、`powershell`/`pwsh`、`cmd`(仅 Windows)或 `auto`。
Windows 上 `auto` 优先选择 Git Bash,找不到时回退 PowerShell。
- 既未声明 `args` 也未声明 `shell` 的已有原生 Hook 继续使用 Reasonix
历史 Shell 命令行为;`shellCommand: true` 仍作为 shell form 的旧写法兼容。
## Manifest v2(扩展)
Reasonix 原生扩展使用精确的 v2 `apiVersion`:
```json
{
"apiVersion": "reasonix.io/plugin/v2",
"name": "example",
"version": "1.0.0",
"description": "Example extension",
"requires": [],
"provides": [
{
"namespace": "plugin/example",
"kind": "interceptors",
"id": "default",
"version": "1.0.0"
}
],
"contributes": {
"skills": ["skills"],
"agents": ["agents"],
"commands": ["commands"],
"prompts": ["prompts"],
"hooks": {},
"mcpServers": {},
"themes": ["themes/*.reasonix-theme"]
},
"runtime": {
"command": "${REASONIX_PLUGIN_ROOT}/bin/example",
"args": [],
"env": {},
"required": true,
"priority": 0,
"intercepts": ["input.receive", "tool.before"],
"replaces": [],
"capabilities": ["interceptors"]
}
}
```
解析规则:
- 原生 `reasonix-plugin.json` 必须声明精确值 `reasonix.io/plugin/v2`。
v1 与缺失版本都会被拒绝;不提供 v1 双读或自动迁移路径。
- v2 是严格的:根对象或 `contributes`/`runtime` 下的任何未知字段都会
报错并指明字段路径,避免拼写错误静默失效。
- v2 的资源发现是显式的。Reasonix 只加载原生 manifest 中声明的 skills、
agents、commands、prompts、hooks、MCP servers、themes 与 runtime;不会隐式
导入根目录 `CLAUDE.md`、`hooks/hooks.json`、`.claude/settings.json` 或
`.mcp.json` 等宿主专用 sidecar。
- minor 别名(如 `reasonix.io/plugin/v2.0`、`v2.1`)及未知 major version
都会被拒绝。
- `requires` 与 `provides` 声明依赖约束和能力上限;Sidecar handshake
不能超出该上限。
- v2 可以同时使用受支持的顶层资源字段(`skills`、`hooks`、
`mcpServers` 等)与
`contributes`:完全相同的路径去重;同名但定义不同的条目报 Manifest
错误并指明键名。
- 所有相对路径与 glob 必须位于插件根目录内:拒绝路径穿越、绝对路径、
逃逸 symlink 和非普通 theme 文件。
新资源类型:
- `prompts` 使用与 commands 相同的模板语义和参数替换,公开名为
`/<plugin>:<name>`;`commands` 保持兼容别名。
- `themes` 是 `.reasonix-theme` 文件,在 Desktop 设置中以只读插件主题
展示(ID 为 `plugin:<plugin>:<theme>`),不会复制进用户主题库。插件
被禁用或卸载时,若当前使用的是它的主题,界面回退到基础样式但保留该
ID,重新安装同一插件后自动恢复。
`runtime` 块声明的是代码型扩展——由 Reasonix 启动并通过 Extension
Protocol(基于 stdio 的 JSON-RPC 2.0,方法索引见
`docs/EXTENSION_PROTOCOL.generated.md`,Go SDK 见 `sdk/go/README.md`)
驱动的 Sidecar 进程:
- `command`/`args`/`env` 仅支持 **exec form**:command 即可执行文件,
绝不经过 Shell 解释;`${REASONIX_PLUGIN_ROOT}` 展开为插件安装根目录。
- `intercepts` 声明要拦截的事件(如 `input.receive`、`tool.before`、
`permission.decision`);`replaces` 声明可以持有的替换槽
(`system_prompt`、`context`、`provider_request`、`provider_response`、
`compaction`、`session_policy`、`permission`、`frontend_events`、
`tool:<name>`、`provider:<ref>`)。同一替换槽在所有已安装插件中只能有
一个 owner,争用会导致构建失败并列出来源。
- `capabilities` 按能力族授权:`interceptors`、`strategies`、`providers`、
`ui`。Sidecar 在握手时声明的任何超出 Manifest 的能力都会被拒绝。
- 扩展提供的模型以 `plugin/<plugin>/<provider>/<model>` 形式出现在模型
选择器中;该 ref 同样可用作 `default_model`(包括首次启动),并可
在 `/model`、Desktop 与 ACP 的模型切换中使用。
**完全信任(Full trust)。** 代码型扩展运行在 Sandbox 之外,继承未过滤
的完整环境:它可以读取完整会话与环境、绕过权限、直接操作本机;扩展在
`permission.decision` 上的 "allow" 可以覆盖宿主的 deny。安装、更新、
替换或 `--link` 一个带有 `runtime` 块的插件**即代表授权**——不会有二次
确认,`--link` 模式在内容变化后自动保持信任。因此安装预览、
`reasonix plugin show`、能力诊断和 Desktop 安装界面都会显著展示
`FULL TRUST` 区块,列出 Runtime 命令、Interceptors、替换槽和
Provider/UI 能力。安装前请确认该区块内容,只安装你完全信任的运行时。
只有通过插件安装流程写入插件状态的 Runtime 才能启动;项目配置无法
声明代码型 Sidecar。
## Codex 与 Claude 兼容
Reasonix 也会读取 `.codex-plugin/plugin.json` 和 `.claude-plugin/plugin.json`。
安装预检会结构化显示“完全兼容 / 部分兼容 / 不兼容”、已映射能力和每个被跳过
的条目。非原生插件如果没有任何可映射能力,会直接阻止安装,不再留下“安装成功
但不可用”的记录。“完全兼容”指清单里声明的每个能力都成功解析并映射到了
Reasonix 的对应实现,并不代表导入 Hook 的每一种运行时决策都被遵守。
`PreToolUse`/`PermissionRequest` 的“拒绝”与 `PermissionRequest` 的“批准”已经
实现;但 Hook 的 `updatedInput`,以及 `PreToolUse` 的 `ask`/`defer` 决策,是
脚本在实际运行时通过 stdout 决定的,并非清单里的静态字段,因此安装阶段无法
据此标记——具体已实现范围见下面 Hook 条目。GitHub 仓库若在
`.claude-plugin/marketplace.json` 中通过 `./plugins/example` 或
`plugins/example` 这类相对字符串列出多个插件,可以直接从仓库根目录安装;
预检会在写入前逐项展示安装动作。填写可选安装名称时,可只选择 marketplace
中的同名插件。对象来源仅接受 GitHub 仓库 URL 加完整 commit SHA;未固定版本的
外部字符串、npm、`strict: false` 以及其他高级 marketplace 协议在整库安装时会
跳过,按名称选中时则直接报错。
对于 Superpowers 和 Claude 风格 skill 包,Reasonix 会映射以下兼容约定。
原生 v2 manifest 只使用显式声明,不应用这些回退规则:
- `skills` 到 Reasonix skill root。Claude 清单若未声明 `skills` 字段,会回退到
约定目录 `skills/`(或 `.claude/skills/`),与 Claude 自身的自动发现一致。
插件 skill 统一以 `/<插件名>:<技能名>` 展示和调用。无歧义的 `/<技能名>`
仍作为隐藏兼容别名接受输入;项目和用户 skill 保留短名称,多个插件导出的
同名 skill 则只能通过各自的限定名称独立调用。这一用户侧命名空间不会改变
模型 skill 索引或 `run_skill` 工具使用的内部短标识。
- `commands/`(以及 `.claude/commands/`)映射为 Reasonix 自定义斜杠命令:每个
`<name>.md` 提示词模板统一以 `/<插件名>:<命令名>` 展示和调用,frontmatter 的
`description` / `argument-hint` 以及 `$ARGUMENTS` / `$1..$N` 替换均生效。
当短名称没有歧义时,`/<命令名>` 仍作为隐藏兼容别名接受输入,但不会出现在
补全、帮助、桌面菜单、ACP 命令发现或提供给模型的命令清单中。用户和项目命令
始终占有自己的短名称;多个插件导出同名命令时不会生成短名称别名。显式自定义
命令也可以占用限定名称,Desktop 插件详情会报告该冲突。原生
`reasonix-plugin.json` 清单也可以通过 `"commands"` 路径列表显式声明。
- `agents/*.md` 映射为插件所属、需要手动调用的子代理配置。Claude 模型别名会继承
当前 Reasonix 模型;内联 `tools` 列表会转换为 Reasonix 工具名,并支持
`mcp__*__search` 这类 MCP 通配符。Agent 使用独立的
`/<插件>:agent:<名称>` 命名空间,因此上游 Agent 与 Skill 同名时不会互相遮蔽。
- 如果存在 `hooks/session-start-codex`,映射为 Reasonix `SessionStart` hook。
- 对 Codex 兼容包,插件根目录的 `CLAUDE.md` 会映射为内置的 `SessionStart`
上下文 hook,Reasonix 会直接读取该文件,不通过 shell 命令。Claude 插件
manifest 会忽略该文件,与 Claude Code 的插件契约保持一致。
- `.claude/settings.json` 和 `hooks/hooks.json` 里的 command hooks 会按同名事件映射。
`matcher`、`args`、`shell`、`async`、`env` 和 timeout 均会保留。Claude 的执行契约
也会完整保留:只要出现 `args`(即使是空数组)就按 exec form 执行,并逐项原样传参;
省略 `args` 才按 shell form 执行,将原始命令交给声明的 Bash 或 PowerShell。
`matcher` 以及 Hook 脚本看到的
`tool_name` 会在 Reasonix 与 Claude 的工具名之间互译(`bash` ↔ `Bash`、
`write_file` ↔ `Write` 等),因此 `"Bash"` 这类 matcher 能正确触发;Reasonix 里所有会
启动子代理的工具(`task`、`read_only_task`、`parallel_tasks`,以及专用的
`explore`/`research`/`review`/`security_review` 包装工具)都会映射到 Claude 唯一的
`Agent` 工具,matcher 里旧名 `Task` 依然可用。`tool_input` 里字段名不同的键也会改名——
每个映射后的 `Agent` 载荷都会包含 Claude 必填的 `prompt` 和 `description`;若
Reasonix 调用省略了可选描述,会补一个稳定的操作标签。
`Read`/`Write`/`Edit`/`MultiEdit` 的 `path` 改成 `file_path`,`NotebookEdit` 的
`path` 改成 `notebook_path`,`Skill` 的 `name`/`arguments` 改成 `skill`/`args`,
当前 `TaskOutput`/`TaskStop` 的 `job_id` 改成 `task_id`,专用子代理包装
工具的 `task` 改成 `Agent` 的 `prompt`,`parallel_tasks` 则会把各子任务的 prompt
合成为 `Agent` 的 `prompt`(原 `tasks` 数组保留)——这样读取
`.tool_input.file_path` 或 `.tool_input.prompt` 的防护 Hook 才不会因为拿到空值
而失败放行。旧的 `BashOutput`/`KillShell` matcher 仍能触发,但下发名称和字段使用
Claude 当前词汇;`bash_output` 会补齐 `TaskOutput` 的非阻塞必填字段,`wait` 也会
映射为 `TaskOutput`,单任务等待时包含 `task_id`,无限等待时省略可选的
`timeout`,而不是谎报 0 毫秒预算。`AskUserQuestion` 会补省略的
`multiSelect:false` 和空选项描述,`TodoWrite` 会用任务内容补省略的 `activeForm`;
`NotebookEdit` 则会从 Reasonix 接受的别名补 `new_source`,删除或空单元格操作补空串。
相对的 `file_path`/`notebook_path` 会按载荷 `cwd` 解析为绝对路径,
与 Claude 文件工具契约一致,前缀匹配的防护 Hook 检查的就是工具实际访问的路径。
`Bash` 的 `tool_response` 按 Claude 的 `{stdout, stderr, interrupted}` 形态下发
(Reasonix 的合并输出放在 `stdout`,失败错误文本作为 `stderr`),官方
security-guidance 插件的 commit/push 检查读取的正是这些字段;其他工具的结果仍按
原样透传。导入 Hook 的
stdin 使用 Claude 兼容的 snake_case 载荷(包括 `hook_event_name`)。宿主会在启动
进程前展开 `${CLAUDE_PLUGIN_ROOT}` 和 `${REASONIX_PLUGIN_ROOT}`,也兼容不带花括号的
`$NAME` 与 Windows `%NAME%` 写法,因此插件相对路径不再依赖目标 shell 的环境变量
语法。Windows 上未显式指定 Shell 的 shell-form Hook 会和 Reasonix Shell 工具一样,
优先选择 Git Bash,找不到时回退 PowerShell;指向带 POSIX shebang 的脚本文件时,
宿主会把 Windows 路径转换为 Bash 可用形式。显式 Bash Hook 以及旧式裸
`sh -c`/`bash -c` Hook 会复用 Git for Windows Bash 探测,即使 Bash 不在
`cmd.exe` 的 `PATH` 中也能执行;带目录的显式解释器路径保持
不变。如果机器确实没有可用 Bash,hook 会返回清晰的依赖提示,而不是本地化的
“无法识别 sh”乱码。通过 `[tools.shell] prefer = "bash"` 和
`path = ".../bash.exe"` 配置的非标准目录或便携版 Bash 也会被显式 Bash Hook 复用。
`reasonix plugin doctor <名称>` 和 `reasonix doctor capabilities` 会在 Hook 首次触发前
报告缺失的 Shell 依赖。旧代码页输出也会在进入界面前转换为 UTF-8。`PreToolUse` 和
`UserPromptSubmit` hook 仍可
通过退出码 2 或退出码 0 时的 JSON 拒绝形态拒绝该次调用(`PreToolUse` 用
`hookSpecificOutput.permissionDecision`,`UserPromptSubmit` 用顶层
`decision:"block"`);导入的 `PermissionRequest` hook 还能直接代答权限弹窗
(拒绝或自动批准,而不只是发通知),通过退出码 2 或
`hookSpecificOutput.decision.behavior` 实现,与 Claude 官方语义保持一致。
`updatedInput` 暂未应用到实际工具调用参数;Hook 的 `if` 条件和 `asyncRewake`
字段也不会被求值。声明其中之一、声明 `Stop`/`SubagentStop` hook(Reasonix 中
不能阻止本轮结束),或 matcher 覆盖三种无法无损表达的输入时,插件都会报告
部分兼容并附具体警告:`WebFetch.prompt`、Reasonix 以 `cell_number` 调用时的
`NotebookEdit.cell_id`,以及 Reasonix `wait` 同时覆盖多个/全部任务时的
`TaskOutput.task_id`。每类结构性缺口在每个 hooks 文件里只报告一次,
通配 matcher 的插件每类缺口只会看到一条警告,而不是每个 hook 一条。
- 插件根目录 `.mcp.json` 会映射为已安装 MCP。Claude 的 `local` 会转换为 stdio;
中文等显示名称会生成稳定内部 ID;重复声明会去重。导入服务器默认
`auto_start=false`,由用户按需连接,避免启动时改变提供给模型的工具 schema。
不支持的 Claude hook item type 会跳过并产生 warning。Reasonix 不会执行第三方安装脚本。
插件 hook 会收到这些环境变量:
- `REASONIX_PLUGIN_ROOT`
- `REASONIX_PLUGIN_NAME`
- `REASONIX_PLUGIN_VERSION`
- `REASONIX_HOME`
- `REASONIX_WORKSPACE_ROOT`
- `CLAUDE_PROJECT_DIR`
- `CLAUDE_PLUGIN_ROOT`
## 桌面端后端方法
Desktop 通过 Wails 方法暴露插件包操作:
- `Plugins`
- `PlanPluginInstall`
- `InstallPlugin`
- `RemovePlugin`
- `SetPluginEnabled`
- `UpdatePlugin`
- `PluginDoctor`
-99
View File
@@ -1,99 +0,0 @@
# 思考语言
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="./REASONING_LANGUAGE.md">English</a>
`agent.reasoning_language` 控制模型服务暴露“可见思考过程”时,Reasonix 希望它优先使用哪种语言。
它不设置最终回答语言,不翻译代码、标识符或文件路径,也不改变模型内部不可见推理。用户在单次提问里明确要求的最终回答语言,仍然优先。
## 为什么需要它
有些用户希望可见思考过程更稳定地使用中文或英文,即使任务本身混合了多种语言。这个设置把这种偏好显式化,同时不改稳定 system prompt,也不改工具 schema。
取值只有三种:
- `auto`:用户原始提问明显为中文时锚定中文,并忽略 `@file` 等注入的引用内容;英文或不确定时不额外注入语言指令。
- `zh`:可见思考过程优先使用简体中文。
- `en`:可见思考过程优先使用英文。
## 桌面端
入口:
```text
设置 -> 模型 -> 使用 -> Agent 运行 -> 思考语言
```
桌面端设置会写入用户级默认值。项目仍然可以通过 `./reasonix.toml` 覆盖。
## CLI 与 TUI
在 shell 或脚本里修改:
```bash
reasonix config reasoning-language auto
reasonix config reasoning-language zh
reasonix config reasoning-language en
```
默认写入用户配置。要写入当前项目的覆盖配置:
```bash
reasonix config reasoning-language --local zh
```
在 `reasonix` 内可以用斜杠命令:
```text
/reasoning-language auto
/reasoning-language zh
/reasoning-language en
```
斜杠命令会写入用户级设置,并立即更新当前 chat controller,后续 turn 生效。它不会改写当前项目的 `reasonix.toml`;如果要写项目级覆盖,请使用带 `--local` 的 shell 命令。
单次 headless 运行也会读取同一设置:
```bash
reasonix run "解释这个模块"
```
## 配置文件
用户级或项目级配置:
```toml
[agent]
reasoning_language = "auto" # auto|zh|en
```
这个设置的配置优先级是:
```text
./reasonix.toml > 用户 config.toml > 内置默认值
```
目前没有为它提供命令行 flag。它更像用户偏好或项目偏好,不适合作为每次运行都传一次的任务参数。
## 缓存影响
`auto` 仍然对缓存友好。用户原始提问明显是中文时,Reasonix 会为这一轮加入同样很小的 `<reasoning-language>` 临时 block;英文或信号不明确时不注入,只复用已有的稳定语言策略。`@file` 等注入的引用内容不会参与这个自动判断。
当设置为 `zh` 或 `en` 时,Reasonix 总是会把一个很小的 `<reasoning-language>` 临时 block 放进本次 user turn。所有模式下,它都不会改变:
- system prompt
- 工具 schema 的字节或顺序
- provider 可见的稳定前缀
因此它能在表达明确偏好的同时,尽量保持高 prompt-cache 命中率。
## 边界
- 只有模型服务暴露可见思考文本时,这个设置才有意义。
- 它是偏好,不是强制翻译层。
- 代码、标识符、文件路径、shell 命令和未翻译技术术语应保留原样。
- 如果用户在某次提问中明确要求最终回答使用某种语言,最终回答仍以用户要求为准。
- 可见思考的语言主要由两个信号锚定:本轮第一段思考的语言,以及工具调用循环中回传给模型的既有思考段落的语言。注入的语言 block 的作用点是让第一段思考落在偏好语言上;第一段站稳后,后续段落通常会自我维持。
- 在被另一种语言主导的长回合里(例如大量英文构建日志、代码或工具输出),后续某一段思考仍可能漂移;一旦漂移,本轮剩余部分通常保持漂移后的语言,中途重申偏好也只能部分拉回。这是模型行为的边界,该设置保持尽力而为。
-147
View File
@@ -1,147 +0,0 @@
# 按提供商划分的推理控制
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="./REASONING_PROVIDERS.md">English</a>
Reasonix 只暴露一个 `/effort` 开关(以及 provider 级的 `effort` / `thinking`
配置字段),但 OpenAI-compatible 后端对*如何*在线上请求思维链(chain-of-thought)
存在分歧。`openai` provider 会按后端调整请求形态;下表是参考依据,说明每个已知
后端使用哪种协议、会采纳或忽略哪些参数。
## 自动识别的后端
这些后端按 Base URL 识别(见 `internal/provider/openai/host.go`),并自动获得
定制的请求形态——无需额外配置。
| Provider | Base URL | 推理控制 | `/effort` 档位 | 备注 |
|-------------------|-------------------------------------------------------------|----------------------------------------------|------------------------------------------|-------|
| DeepSeek V4 Flash | `api.deepseek.com`、`*.deepseek.com` | `thinking.type` + `reasoning_effort`(深度) | `auto`、`disabled`、`low`、`high`、`max` | 默认开启思考;`disabled` 通过 `thinking.type=disabled` 关闭。兼容性输入 `medium` 归一化为 `high`,`xhigh` 归一化为 `high`。 |
| DeepSeek V4 Pro | `api.deepseek.com`、`*.deepseek.com` | `thinking.type` + `reasoning_effort`(深度) | `auto`、`disabled`、`high`、`max` | 默认开启思考;`disabled` 通过 `thinking.type=disabled` 关闭。兼容性输入 `low`/`medium` 归一化为 `high`,`xhigh` 归一化为 `max`。 |
| MiniMax M3 | `api.minimaxi.com`、`*.minimaxi.com` | `thinking.type`(`adaptive`\|`disabled`) | `auto`、`adaptive`、`disabled` | 无深度档位;`reasoning_effort` 会被省略。 |
| Zhipu GLM | `open.bigmodel.cn` / `*.bigmodel.cn`、`api.z.ai` / `*.z.ai` | `thinking.type`(`enabled`\|`disabled`) | `auto`、`enabled`、`disabled` | **端点会静默忽略 `reasoning_effort`**,因此推理完全由 `thinking.type` 驱动。 |
## 显式的逐模型档位
| Provider/模型 | Base URL | 推理控制 | `/effort` 档位 | 备注 |
|----------------------------|--------------------------------------------|-----------------------------------------------|-------------------------------|-------|
| Kimi CN/Global `kimi-k3` | `api.moonshot.cn/v1`、`api.moonshot.ai/v1` | `reasoning_effort` | `low`、`high`、`max` | 始终思考;默认 `max`。Reasonix 会回放完整的 assistant 消息、使用 `max_completion_tokens`,并省略 K3 固定的采样字段。 |
| 自定义 Kimi K3 网关 | 任意 OpenAI-compatible K3 端点 | `reasoning_effort` | `low`、`high`、`max` | 设置 `reasoning_protocol = "kimi-k3"`,显式启用 K3 的完整消息回放与请求形态。 |
| OpenCode Go `kimi-k3` | `opencode.ai/zen/go/v1` | `reasoning_effort` | `high`、`max` | 中转站专属档位;默认 `max`,并保留中转站标准的 OpenAI-compatible 请求形态。 |
| Token Rhythm DeepSeek V4 | `tokenrhythm.studio/v1` | DeepSeek `thinking.type` + `reasoning_effort` | 模型专属的 DeepSeek 档位 | 通过预设的模型覆盖选择,与网关主机无关。 |
| Token Rhythm GLM 5/5.1/5.2 | `tokenrhythm.studio/v1` | GLM `thinking.type`(`enabled`\|`disabled`) | `auto`、`enabled`、`disabled` | 通过预设的模型覆盖选择;`reasoning_effort` 会被省略。 |
在 Token Rhythm 端点上,精确的 GLM 模型 ID(`glm-5`、`glm-5.1` 和 `glm-5.2`)
会自动选择官方的 GLM 请求形态,即使现有配置没有 `reasoning_protocol` 字段也
如此。端点检查让不相关的混合模型网关保持向后兼容。对于别名和自定义模型 ID,
仍可在一个 `model_overrides` 条目中显式设置 `reasoning_protocol = "glm"`。
GLM 思考开启时,Reasonix 会按 GLM 交错与保留思考的要求,在后续历史中原样保留
并返回原始 `reasoning_content`。
如果自定义网关提供 Kimi K3,可在 provider 编辑器的高级设置中将推理协议选择为
**Kimi K3 推理**,或直接配置:
```toml
[[providers]]
name = "my-kimi-gateway"
kind = "openai"
base_url = "https://my-gateway.example.com/v1"
model = "kimi-k3"
api_key_env = "MY_KIMI_API_KEY"
reasoning_protocol = "kimi-k3"
```
当网关域名无法被安全自动识别时,需要这个显式协议。它会在后续 assistant 历史中
保留 `reasoning_content`、使用 `max_completion_tokens`,并省略 K3 固定的采样字段。
不要把它加到精选的 OpenCode Go 预设中:该中转站有自己的 `high`/`max` 档位,
并且有意保持标准 OpenAI-compatible 请求形态。
启用该协议后,Reasonix 固定展示 K3 的 `auto`/`low`/`high`/`max` 档位,协议默认值
为 `max`;已有的 `supported_efforts` 配置仍会保留,但不会覆盖 K3 协议档位。
## DeepSeek Anthropic-compatible 端点
默认官方 DeepSeek provider 指向 `https://api.deepseek.com/anthropic`。
新建的官方条目使用原生 Messages API 路径并开启 provider 侧 `web_search`;已有显式
provider(包括旧的 `deepseek-anthropic` 条目)保留其原协议。Reasonix 会发送
`thinking.type=enabled|disabled` 与 `output_config.effort`,回放历史工具调用轮次
中未签名的 DeepSeek 思考块,省略不支持的图片,并依赖 DeepSeek 的自动前缀缓存,
而不是被忽略的 `cache_control` 标记。
该预设暴露当前模型专属的 effort 档位:Flash 支持 `auto`、`disabled`、`low`、
`high` 和 `max`,而 Pro 暴露 `auto`、`disabled`、`high` 和 `max`,因为其当前的
`low` 输入会映射到 `high`。Anthropic-compatible 端点在线上接受 `low|high|max`。
遗留的 `medium` 归一化为 `high`;遗留的 `xhigh` 对 Flash 归一化为 `high`、对
Pro 归一化为 `max`。Claude Opus 别名使用 Pro 映射,而 Sonnet/Haiku 以及不支持
的模型名遵循 DeepSeek 文档记载的 Flash 回退。
## Anthropic-compatible 端点(`kind = "anthropic"`)
Messages API 上的推理深度依赖 Anthropic 自己定义的字段:`thinking.type=adaptive`
配合 `display=summarized`、`output_config.effort`,以及被抬高的自动输出预算。
第一方端点——`api.anthropic.com`,或未填 `base_url` 的条目——完整实现了这些,
因此暴露完整的 `auto`/`low`/`medium`/`high`/`xhigh`/`max` 菜单,并且选择档位本身
就会开启扩展思考。不会再有任何一侧为了"让这个旋钮生效"而往配置里写 `thinking`。
中转/兼容网关不会被假定实现了其中任何一项。未声明时它暴露二元菜单
(`auto`/`enabled`/`disabled`),并且只收到 `thinking.type`——不发 `display`、
不发 `output_config`、不抬高预算——因为这些正是中转站会用 4xx/5xx 拒绝的字段。
为这类端点持久化过的深度档位会转为休眠而不会上线;配置里的
`thinking = "adaptive"`——这是只有 Anthropic 契约才定义的取值——会回落为服务端
默认(即不发送该字段)。`/effort enabled` 仍可通过所有兼容网关都实现的开关
显式打开思考。
要让某个端点选择加入,就显式声明契约:指定协议,或声明一份深度档位表。
```toml
[[providers]]
name = "my-claude-relay"
kind = "anthropic"
base_url = "https://relay.example.com/anthropic"
model = "claude-opus-4-8"
api_key_env = "MY_RELAY_API_KEY"
reasoning_protocol = "anthropic" # 或:supported_efforts = ["low", "high", "max"]
```
从精选预设安装的条目(Kimi Coding Plan、MiMo、GLM/Z.AI coding plan、Qwen coding
plan、StepFun)无需再声明:条目里记录的 `preset_id` 就是那份审核过的出处,因此它
会保留预设本身声明的 thinking 模式。
## 其他所有后端(标准 `reasoning_effort`)
任何其他 OpenAI-compatible 后端都会回退到标准的 `reasoning_effort` 档位
(`low`\|`medium`\|`high`)。解析出的 provider/模型条目可以显式声明不同的支持
档位;在这种情况下,Reasonix 会保留这些声明的值,而不是套用通用上限。精选的
逐模型能力元数据可以像上面展示的那样选用其他档位。
以下主流提供商经调研无需**特殊处理**,因为它们已经遵循标准约定:
Qwen (`dashscope.aliyuncs.com`)、Yi (`api.01.ai`)、SiliconFlow
(`api.siliconflow.cn`)、Stepfun (`api.stepfun.com`)、Groq (`api.groq.com`)、
Together (`api.together.xyz`)、OpenRouter (`openrouter.ai`)、Perplexity
(`api.perplexity.ai`)、xAI (`api.x.ai`)。
对于使用二值 `thinking.type` 开关但**未被**自动识别的后端,在 provider 条目上
设置与厂商无关的 `thinking` 字段:
```toml
[[providers]]
name = "my-glm-proxy"
kind = "openai"
base_url = "https://my-gateway.example.com/v1"
model = "glm-4.6"
api_key_env = "MY_API_KEY"
thinking = "disabled" # enabled | disabled — 发送 thinking.type
```
## 故障排查
如果模型在你要求它不要思考时仍在思考(或反过来):
1. 对照上表——后端可能**忽略**你设置的参数(例如 Zhipu 会忽略
`reasoning_effort`;改用 `thinking`/`/effort`)。
2. 如果后端未被自动识别,就显式设置 `thinking` 字段。
3. 如果后端完全使用非 OpenAI 协议(例如百度文心),`openai` kind 无法驱动它
的思考模式——那需要专门的 provider kind。
区分“provider 忽略字段”与 Reasonix 自身的 bug 从这里入手:Reasonix 发出的
请求形态按表格固定,因此表格与实际行为不一致时,问题在提供商而不是 Reasonix。
-73
View File
@@ -1,73 +0,0 @@
# 恢复与诊断(v1.20+)
Reasonix 不再提供产品化的 `reasonix-guard` 恢复壳。崩溃记录、pending 更新状态
和配置问题**不会**在下次启动时强制进入全局安全模式。
## 请使用这些命令
```text
reasonix doctor
reasonix doctor repair
reasonix crash report # 视构建是否包含而定
```
- **doctor**:检查配置、桌面派生状态与常见安装问题,不加载 Wails。
- **doctor repair**:在用户明确选择后做安全修复。
- 崩溃上报仍为用户授权后发送,且不会改变下次启动模式。
## 安装布局(v1.20+)
Windows / Linux 使用版本目录:
```text
InstallRoot/
reasonix-launcher[.exe]
Reasonix.exe # Windows 便携/开始菜单别名
reasonix[-cli.exe]
current.json
versions/<version>/
reasonix-desktop[.exe]
reasonix-cli[.exe]
reasonix-update-helper[.exe]
```
薄启动器只读取 `current.json` 并启动当前 Desktop,**不会**自动选旧版本或进入
安全模式。
## 从 1.18–1.19.x 升级
若旧客户端卡在 pending update 或安全模式循环:
1. 从官方下载页获取最新签名安装包。
2. 直接覆盖当前安装(Windows:双击安装;macOS:替换 `Reasonix.app`)。不要先卸载:
保留原安装根目录,兼容迁移程序才能证明遗留事务属于哪个安装。
3. 启动 Reasonix 一次,确认“设置 > 更新”显示的版本正确,再尝试下一次应用内更新。
4. 兼容载荷中可能仍带有名为 `reasonix-guard` 的**一次性迁移程序**,它只把旧平铺
布局写成 `current.json` 后自删除,并非旧 Guard 产品。
正式恢复路径**不要求**手工删除 `pending-update.json`、锁文件或 AppData。
## 应用内更新卡住
若「设置 → 更新」或顶部横幅提示上次更新尚未完成(含 `pending update already exists`、
`awaiting startup health`、`handoff backup` 等错误):
1. 在横幅或设置里点 **「放弃上次更新」**,再点 **「重试」**。
2. 若没有该按钮或操作失败:完全退出 Reasonix 后重新启动一次(让启动路径提交或清理试用事务),再试应用内更新。
3. 若仍失败:从官方下载页获取最新签名安装包,**直接覆盖安装**当前版本,不要先卸载。
4. macOS 还需在「系统设置 → 隐私与安全性 → App 管理」中允许 Reasonix;若
`Reasonix.app.reasonix-update-backup` 因 TCC 无法删除,请改走官方安装包覆盖路径。
如果 Windows 安装器显示 `Reasonix layout activation failed`,请展开安装详情并复制
`Reasonix layout activator output:` 下方的内容。当前安装器会保留 activator 的具体错误,
不再只显示 exit code 1。
## macOS
macOS 仍由 LaunchServices 直接启动 Wails App 包;更新原子替换签名 `.app`,无
Guard 进程。
替换后的窗口真正显示后,Reasonix 只会提交启动前捕获的精确 pending 事务。对于缺少
备份摘要、或备份已经丢失的旧事务,只有在确认当前运行程序确实属于目标 App 包后才会
自动退出该事务;仍存在但身份未知的备份和原事务会被归档保留,不会删除,也不会被当作
可信来源自动回滚。
-120
View File
@@ -1,120 +0,0 @@
# Releasing Reasonix
Reasonix has one user-facing release line: the official `X.Y.Z` version. The
release engine keeps the proven Stable publication topology: three immutable
Git tags on one `main-v2` commit and one protected orchestrator.
| Surface | Immutable tag | Public result |
| --- | --- | --- |
| CLI | `vX.Y.Z` | GitHub Release and Homebrew |
| npm | `npm-vX.Y.Z` | root and platform packages; `latest`, `canary`, and `next` compatibility aliases |
| Desktop | `desktop-vX.Y.Z` | signed GitHub Release, immutable R2 directory, and `latest/latest.json` |
The three tags are implementation identities, not user-selectable channels.
They must always resolve to the same commit and may never be moved or deleted.
The Go SDK module (`sdk/go`) is versioned independently of the three product
surfaces: its tags look like `sdk/go/vX.Y.Z` (first: `sdk/go/v1.0.0`) and
point at the release commit that first shipped the corresponding Extension
Protocol major. SDK tags do not trigger product releases, do not move, and
do not change the three-tag contract above.
## Daily release flow
The normal developer path has one version input, one reviewed Notes PR, one
terminal command, and one environment approval:
1. Open Actions → **Prepare release** and enter `X.Y.Z`.
2. Review and merge the generated bilingual release-notes PR.
3. From an authenticated maintainer checkout, run:
```sh
./scripts/release-stable.sh X.Y.Z
```
4. Approve the resulting **Release stable** run once in the `release`
environment.
5. Wait for its postflight to verify CLI, npm, Desktop, R2, Homebrew, and the
changelog.
If repository policy prevents Actions from opening the Notes PR, the workflow
still pushes `release-notes/vX.Y.Z` and prints this recoverable handoff:
```sh
gh pr create --repo esengine/DeepSeek-Reasonix \
--base main-v2 --head release-notes/vX.Y.Z --fill
```
Do not rerun Notes generation merely because PR creation was denied.
## What the tag helper proves
`scripts/release-stable.sh` fails before creating any public ref unless:
- the version is canonical `MAJOR.MINOR.PATCH`;
- remote `main-v2` is the commit that introduces or updates the complete,
reviewed Stable catalog record;
- exact-commit `main-v2` CI completed successfully;
- `vX.Y.Z`, `npm-vX.Y.Z`, and `desktop-vX.Y.Z` are all absent.
It then pushes a no-op guard for that exact `main-v2` SHA and all three
lightweight tags with one atomic Git transaction. If `main-v2` advanced while
CI was running, the complete transaction is rejected and no version tag is
consumed. A partial tag set is therefore not a normal failure mode. The
`vX.Y.Z` event starts the existing protected Stable relay; maintainers do not
dispatch child CLI, npm, or Desktop publishers.
## Publication and approval
The protected Stable workflow re-resolves all three tags to one SHA on
`main-v2` history, revalidates that normal candidates introduced their reviewed
Notes and passed exact-SHA push CI, and runs the cache guard before requesting
the sole human approval. `main-v2` may safely advance after the atomic tag
transaction without invalidating that candidate. After approval it performs a
no-publication SignPath preflight, then runs CLI, npm, and Desktop publishers
against the immutable candidate.
The npm publisher advances `latest`, `canary`, and `next` to the same official
version. `canary` and `next` remain only so historical scripts continue to
install a supported build; they are not testing channels and are not advertised.
No custom GitHub App, App private key, repository-owner setting change, manual
tag UI, or child-workflow approval is required.
## Recovery
For a partial Stable publication, open **Release stable** on protected
`main-v2`, enter the existing `vX.Y.Z`, select only the missing surfaces, and
approve `release` once. Recovery accepts only an immutable three-tag set that
remains on `main-v2` history. It must reuse matching public content and fail
closed on conflicting checksums, signatures, manifests, npm provenance, or R2
objects.
Never move, delete, or recreate a published tag. Ship product corrections as a
higher patch version.
## Retired prerelease paths
Normal Preview, Canary, and RC publication entrypoints are disabled. Historical
tags, Releases, package versions, changelog pages, and the final bridge endpoints
remain available for compatibility, but they do not appear in current download
navigation or release preparation.
Old CLI and Desktop channel settings resolve to the official line. Frozen
Preview endpoints continue to lead old clients to the bridge build, which can
then upgrade to the current official release.
## First release after cutover
For the first release after this change, independently prove:
- the three tags resolve to the reviewed Notes merge SHA;
- both GitHub Releases contain their complete expected assets;
- npm root and all six platform packages report that SHA and
`latest == canary == next`;
- R2 immutable and latest manifests are byte-identical and every URL works;
- Homebrew and reasonix.io show the same version;
- old bridge clients can upgrade to the official release.
The release is incomplete until every public surface reaches a terminal,
verified state.
-272
View File
@@ -1,272 +0,0 @@
# Context Engine v2:指令、记忆与检索
Context Engine v2 为 Reasonix 提供两个权限不同的持久上下文层:
- **常驻指令**定义智能体必须怎样工作。
- **背景记忆**保存未来可能有用、但也可能过时的事实。
把两者分开是最重要的设计原则:一个事实不应静默升级成命令;一条长期规则也不应依赖
检索是否恰好命中。
## 选择正确的层
| 放在哪里 | 适合存放 | 示例 |
| --- | --- | --- |
| `AGENTS.md`、`REASONIX.md` 或 `CLAUDE.md` | 每个相关回合都必须存在的规则 | 必跑测试、仓库边界、评审约定 |
| 项目记忆 | 只适用于当前 workspace 的持久事实 | 发布分支、代码中看不出的服务约束、项目工单 URL |
| 全局记忆 | 明确需要在所有 workspace 可用的事实 | 用户显式选择为全局的偏好 |
| 会话历史 | 原始措辞、工具输出,或尚未沉淀为稳定事实的决定 | 昨天的报错、已放弃的方案 |
指令文件应保持简短。它们属于 cache-stable prompt prefix,多写的一段内容会被每个回合携带。
可以按需发现的事实应放进记忆。
一个最小项目文件通常就够了:
```markdown
# Build and verify
- Run `go test ./...` before reporting completion.
- Do not edit generated files under `desktop/frontend/wailsjs/`.
- Keep public API changes backward compatible.
```
在 CLI 中,`/remember <note>` 和 `# <note>` 会直接把内容追加到项目指令文档。它们是
常驻指导的快捷方式,不是 agent 用来创建背景事实的 `remember` tool。
## 指令解析
Reasonix 识别 `REASONIX.md`、`AGENTS.md`、`CLAUDE.md`,以及对应的 `.local.md`
变体。它先加载 Reasonix home 下的用户全局指令,再从 workspace root 逐级走到目标路径;
在每个目录内,先加载普通文件,再加载该目录的 `.local.md`。
更深目录高于更浅目录;同一目录内 local 变体高于普通文件,因此越靠后的条目冲突时优先。
用户当前请求始终是最高优先级的用户指令。展开后正文完全相同的文件会去重,并保留更具体
的来源。
指令文件可用独占一行的相对路径导入另一个文件:
```markdown
@docs/agent-testing.md
```
导入按确定性顺序展开、去重,最多五层,并被限制在源指令文件拥有的目录内。绝对路径、
父目录逃逸、符号链接逃逸、不可读文件和循环引用都会被拒绝并形成诊断,不会被静默信任。
用下面的命令查看真实解析结果:
```text
/memory instructions
```
它会显示加载优先级、scope、目标目录、imports 和 diagnostics。桌面 Context Center
展示同一套 provenance。
## 背景事实模型
每条事实都是一个 Markdown 文件,包含:
- 不变的 `id`;
- 单调递增的 `revision`;
- `created_at`、`updated_at` 时间;
- 便于阅读的 name、title 和 description;
- 相互独立的 `type` 与 `scope`;
- 可选的检索 `keywords` —— 关键词的同义词与双语别名,让换一种说法或换一种
语言的提问也能命中这条事实;
- 可选的 `subject_key` —— 点号分隔的键,标明这条事实回答的是哪个问题
(`project.package_manager`、`user.response_style`);
- Markdown 正文。
`type` 表示内容类别:
- `user`:用户身份或偏好;
- `feedback`:关于怎样工作以及原因的反馈;
- `project`:代码库本身无法直接得出的项目目标或约束;
- `reference`:URL、工单 ID 等外部资源。
`scope` 表示生效范围:
- `project` 是安全默认值;
- `global` 必须显式选择。
type 不推导 scope。项目反馈仍只属于项目,全局 reference 仍然是 reference。
subject key 是知识冲突模型:同一 scope 内每个 subject 至多一个 active 值。对已被
占用的 subject 再存新事实会被拒绝并给出持有者的 id——于是 "npm → pnpm" 成为同一
事实的新 revision,而不是两条互相矛盾、同时活跃的事实。`/memory subjects` 列出在用
的 key;回答同一 subject 的事实在覆盖与召回抑制中视为等价,与它们的 name/title 无关。
当等价的项目事实和全局事实同时存在时,自动召回使用项目事实。Context Center 和
`/memory` 仍展示两者,并解释覆盖关系,而不是删除或隐藏任何来源。
第三个维度 `activation` 与前两者正交:`relevant`(默认)表示事实只走检索;`pinned`
表示正文在会话开始时快照进低优先级稳定指导区。pin 必须是用户显式选择(`/memory pin
<id-or-name>`,或明确要求助手),且 pinned 正文总量上限 1,500 字符——在 pin 时强制
执行,超限会提示把"永远必须遵守的规则"移入 REASONIX.md/AGENTS.md instructions。
一个事实要么 pinned(在前缀里)、要么 relevant(可被召回):不会两者皆是,也不会
两者皆非。
为兼容旧数据,早于该字段的全局 `user`/`feedback` 事实保持 pinned,直到显式 unpin。
存在等价项目事实时,它会在稳定前缀构建前屏蔽对应的全局 pinned 指导,因此"项目覆盖
全局"不依赖后续查询是否恰好触发召回。
## 自动召回
每个真实用户回合开始前,Reasonix 会用原始用户消息搜索 active facts。宿主追加给 provider
的上下文不会反过来污染查询。选中的事实作为有预算、低权限的后缀追加到本轮 user turn,
不会修改 system prompt 或工具 schema。
召回策略刻意保守:
- “继续”这类泛化回合不触发召回;
- 用 BM25 排序有区分度的词法命中(CJK 文本按双字 bigram 匹配,命中需要真实的
词语重叠,零散的常用字不算);
- 项目事实有轻微相关性加权;
- 过期事实只降权,不静默删除;
- 本轮存在等价项目事实时,不再注入对应的全局 fallback;
- 已经作为稳定指导存在的全局 `user` / `feedback` 不会被自动召回重复注入;
- 默认最多四条事实、2,400 字符;
- provider 可见块不包含 fact storage path,snippet 中的 home directory 前缀会替换为
`<local-home>`。
freshness 默认按事实类型计算:
| 类型 | fresh | current | 超过多久为 stale |
| --- | ---: | ---: | ---: |
| `reference` | 14 天 | 45 天 | 45 天 |
| `project` | 30 天 | 180 天 | 180 天 |
| `user`、`feedback` | 90 天 | 365 天 | 365 天 |
类型只是默认值,不代表事实的真实易变性——README 地址可能三年不变,release 分支可能
三天就失效。显式 `volatility` 会覆盖类型窗口:`volatile`(7 / 30 天)、`stable`
(90 / 365 天)、`evergreen`(永不老化)。两个可选时间戳进一步细化:`expires_at`
是硬边界——过期后事实状态为 `expired`,完全不再被自动召回(显式搜索仍可见);
`last_verified_at` 由 `/memory verify <id-or-name>` 或助手重新确认事实时打戳,
在不改变 `updated_at` 含义的前提下续期新鲜度时钟。
freshness 是提醒和排序信号,不代表事实真假。召回文本会明确告诉模型:内容可能错误或过期,
不能覆盖当前请求和常驻指令。
查看最近一次决定:
```text
/memory recall
```
trace 包含 query、选中的 ID/revision、score、命中原因、freshness、预算使用量、
omitted 数量和 suppressed 原因。
需要更深检索时仍可使用只读 `memory` tool 的 `search`、`read`、`list`。需要原始措辞或
工具输出时,应使用 `history`。
## 安全写入与确认
普通路径零配置。只有同时满足以下条件时,Reasonix 才可以自动创建一条新记忆:
- 当前父 controller 拥有本项目 memory store(可以是交互式,也可以是顶层 headless,但不能是子智能体);
- type 被显式标为 `project` 或 `reference`;
- scope 为 project 或省略;
- 操作是纯创建,不是更新;
- 正文不超过自动写入预算;
- 未检测到凭据、secret、私钥或邮箱;
- 不存在同名、同 title 或同 description 的事实。
授权是一次性的,存储层还会强制 create-only,因此评估后并发出现的事实也不会被覆盖。
其余情况仍需显式确认:
- 全局事实;
- `user` 偏好和 `feedback`;
- 更新已有 ID 或 revision;
- 可能重复的内容;
- 敏感或超长内容;
- 所有 `forget` 操作。
Auto 和 Yolo 不会绕过这些确认。Guardian 和 permission hook 不能替用户批准。顶层
headless controller 只能使用上述同一个一次性低风险创建路径;子智能体以及不拥有该作用域
controller 的 headless surface 会 fail closed,其他记忆变更仍必须有交互式确认界面。
用户直接在 Context Center、`/remember`、restore 或 recover 命令中发起的操作,本身就是
显式用户动作,不会再增加一次审批。
## Revision、归档与恢复
更新事实时,旧版本会先保存为不可变快照。过期的 `expected_revision` 会被拒绝,不会覆盖
更新后的内容。
恢复旧 revision 不会原地倒退存储,而是把所选内容复制成一个更高的新 revision,保持
单调审计链:
```text
/memory revisions <id-or-name>
/memory restore <id-or-name> <revision>
```
`forget` 会把事实移出 active recall 并放入 `.archive/`。恢复只接受当前 store 拥有的
archive entry,拒绝符号链接和路径逃逸,拒绝 ID/name 冲突,也绝不覆盖 active file:
```text
/memory archived
/memory recover <archive-path>
```
恢复出的内容同样成为一个更高的新 revision。Restore 和 recover 会通过一次 turn-tail note
立即作用于当前会话,并在下次会话自然进入稳定 prefix。
## 零配置建议
打开桌面端 Suggestions tab 时,会自动扫描近期本地用户回合,不需要设置开关。它会提出:
- 从明确偏好、约束和项目约定中提取的长期记忆候选;
- 从重复工作流模式中提取的 Skill 候选。
扫描使用原始用户内容,并与两个 scope 的 facts 和已加载指令正文去重;扫描本身绝不写入。
每个候选都展示 evidence,必须由用户显式接受。远程 workspace 会 fail closed:远端不提供
能力时,Reasonix 不会回退读取桌面机器的本地 session 或 memory。
## 管理界面
直接运行 `/memory` 会显示两个 scope 的全部 active facts,包括 ID、revision、type、
scope、freshness 与存储来源。CLI、Desktop 和 remote workspace 都提供结构化补全。
| 命令 | 结果 |
| --- | --- |
| `/memory` | 指令、事实和 archive 综合摘要 |
| `/memory instructions` | precedence、目录、imports、diagnostics |
| `/memory recall` | 最近一次自动召回 trace |
| `/memory revisions <ref>` | active fact 与不可变历史 |
| `/memory restore <ref> <revision>` | 恢复为一个新 revision |
| `/memory archived` | archive facts 与路径 |
| `/memory recover <path>` | 把当前 store 拥有的 archive 恢复为新 revision |
Context Center 用图形界面展示同一模型,还会显示冲突和 project-over-global 解释。
## 升级兼容
Context Engine v2 会自动升级旧 store,不需要设置:
- 没有 ID 的旧事实获得确定性的 `legacy-*` identity;
- 缺少 revision 的事实从 revision 1 开始;
- 缺少 scope 时,根据所在 project/global 目录推导;
- migration 幂等,只写入一次新 metadata;
- 新旧版本共享 state root 时,兼容路由字段可避免旧客户端把事实移错目录;
- 旧 `MEMORY.md` 作为派生数据处理,并根据事实文件重建;
- 旧 Memory v5 `<memory-compiler-execution>` transcript 仍可读取,退役的
`[agent].memory_compiler` 设置会被移除。
不需要 vector database、embedding service、setup wizard 或手动 re-index 命令。
## Cache 与隐私契约
- 常驻指令和派生 memory index 在会话开始时进入稳定 prefix。
- Provider 可见的指令 provenance 只使用稳定的 `workspace/...` 与 `user/...` 标签;绝对来源路径
和 store 路径仅保留在本地诊断中。
- Provider 可见的 memory tool result 只使用稳定的 `project/<name>.md` 与
`global/<name>.md` 引用。这些引用可直接用于 read、update、revision 和 archive;即使两个
scope 中存在同名事实,也会精确定位;Context Center 和本地恢复诊断仍保留真实存储路径。
- 动态召回和会话中途改动只追加到当前 user turn。
- diagnostics 不进入 provider request。
- 自动召回不暴露 fact storage path,并替换 snippet 中的 home directory 前缀。
- 外部审批通知只收到工具名,不收到记忆正文。
- 远程管理只使用远程 controller 的 memory catalog,绝不回退读取桌面本机 store。
这样既保持 provider-visible prefix 稳定,也让动态上下文可解释、可恢复。
-370
View File
@@ -1,370 +0,0 @@
# 会话引用功能架构文档
> GitHub Issue: https://github.com/esengine/DeepSeek-Reasonix/issues/3185
## 1. 功能需求
在当前会话中使用 `@` 引用其他会话的聊天记录作为内容发送给 AI。
### 1.1 用户场景
- 用户在当前会话中想引用之前会话的对话内容
- 用户想让 AI 参考之前的对话上下文
- 用户想合并多个会话的讨论结果
### 1.2 预期行为(P0-MVP)
```
输入 @
→ 显示原有菜单(文件/目录列表)
→ 菜单顶部新增 "past:chats" 选项
→ 选择 past:chats
→ 切换到历史会话列表
→ 选择一个会话
→ Composer 上方显示"已引用会话"
→ 发送时把该会话历史内容附加到当前 prompt/context
```
---
## 2. 现有代码结构分析
### 2.1 核心文件
| 文件 | 作用 |
|------|------|
| `desktop/frontend/src/components/Composer.tsx` | 输入框组件,包含 @ 功能 |
| `desktop/frontend/src/components/FileMenu.tsx` | @ 文件菜单组件 |
| `desktop/frontend/src/components/HistoryPanel.tsx` | 历史会话面板 |
| `desktop/frontend/src/lib/bridge.ts` | 前后端通信接口 |
| `desktop/frontend/src/lib/types.ts` | 类型定义 |
### 2.2 现有 @ 功能实现
**Composer.tsx 第 257-337 行** 实现了文件引用功能:
```typescript
// --- @ file references ---
const atRaw = useMemo(() => {
const m = /(?:^|\s)@([^\s]*)$/.exec(text);
return m ? m[1] : null;
}, [text]);
// 文件匹配结果
const atMatches = useMemo(() => {
// 过滤本地目录和搜索结果
}, [atRaw, atFrag, entries, searchEntries]);
// 菜单模式判断
const menuMode: "slash" | "slasharg" | "at" | null = ...;
// 渲染文件菜单
{menuMode === "at" && <FileMenu items={atMatches} ... />}
```
### 2.3 已有的会话 API(可复用)
```typescript
interface AppBindings {
// 会话列表
ListSessions(): Promise<SessionMeta[]>;
// 会话操作(可复用读取历史)
PreviewSession(path: string): Promise<HistoryMessage[]>;
}
```
---
## 3. P0-MVP 实施方案
### 3.1 设计思路
在现有的 `@` 菜单中添加 "past:chats" 选项,而不是创建新的 `@session:` 语法。
**菜单结构:**
```
@
├── 📁 past:chats ← 新增:选择后显示历史会话列表
├── 📁 src/
├── 📁 docs/
├── 📄 README.md
└── ...
```
### 3.2 实施路线
```
第一步:后端加搜索接口
↓
第二步:前端 bridge.ts 暴露接口
↓
第三步:在 @ 菜单中添加 "past:chats" 选项
↓
第四步:选择 past:chats 后切换到会话列表
↓
第五步:选择会话后添加到引用区域
↓
第六步:发送时附加会话上下文
```
### 3.3 最小改动文件清单
```
desktop/frontend/src/lib/types.ts — 添加 SessionReference 类型
desktop/frontend/src/lib/bridge.ts — 添加 SearchSessions API
desktop/frontend/src/components/Composer.tsx — 扩展 @ 菜单逻辑
desktop/frontend/src/components/FileMenu.tsx — 扩展菜单支持会话项
desktop/app.go — 添加 SearchSessions 方法
desktop/sessions.go — 实现会话搜索逻辑
```
### 3.4 类型定义
```typescript
// types.ts
export interface SessionReference {
path: string;
title: string;
preview?: string;
turns?: number;
createdAt?: number;
lastActivityAt?: number;
messages?: HistoryMessage[]; // P0 先不存,发送时再拉取
}
```
### 3.5 API 设计
```typescript
// bridge.ts
interface AppBindings {
// 新增:搜索会话
SearchSessions(query: string): Promise<SessionMeta[]>;
// 已有:读取会话历史(复用)
PreviewSession(path: string): Promise<HistoryMessage[]>;
}
```
### 3.6 前端逻辑修改
**Composer.tsx 修改:**
```typescript
// 1. 添加状态
const [showPastChats, setShowPastChats] = useState(false);
const [pastChats, setPastChats] = useState<SessionMeta[]>([]);
const [sessionRefs, setSessionRefs] = useState<SessionReference[]>([]);
// 2. 修改 @ 菜单渲染
{menuMode === "at" && (
showPastChats ? (
// 显示会话列表
<SessionMenu
items={pastChats}
activeIndex={active}
onPick={pickSession}
onHover={setActive}
/>
) : (
// 显示文件列表(原有逻辑)
<>
<button
className="slashmenu__item slashmenu__item--special"
onMouseDown={() => {
setShowPastChats(true);
app.ListSessions().then(setPastChats);
}}
>
<MessageSquare size={13} />
<span className="slashmenu__name">past:chats</span>
<span className="slashmenu__desc">引用历史会话</span>
</button>
<FileMenu items={atMatches} ... />
</>
)
)}
// 3. 选择会话后的处理
const pickSession = (session: SessionMeta) => {
// 添加到引用区域
setSessionRefs(prev => [...prev, {
path: session.path,
title: session.title || session.preview || "Untitled",
preview: session.preview,
turns: session.turns,
createdAt: session.createdAt,
lastActivityAt: session.lastActivityAt,
}]);
// 重置状态
setShowPastChats(false);
setText(""); // 清空输入框
};
// 4. 发送时附加会话上下文
const handleSubmit = async () => {
let context = "";
if (sessionRefs.length > 0) {
context = "以下是用户引用的历史会话上下文:\n\n";
for (const ref of sessionRefs) {
const messages = await app.PreviewSession(ref.path);
const limited = limitMessages(messages, 30, 20000);
context += formatSessionContext(ref.title, limited);
}
context += "\n\n当前用户问题:\n";
}
onSubmit(context + text);
};
```
### 3.7 限制策略
```
最多引用最近 30 条消息
或最多 20k 字符
超出部分截断,并提示"已截断"
```
### 3.8 发送时的消息格式
```
以下是用户引用的历史会话上下文:
[会话:修复登录 bug]
用户:...
助手:...
用户:...
当前用户问题:
...
```
---
## 4. 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Composer.tsx │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ @ 菜单逻辑 │ │
│ │ - 原有:文件/目录列表 │ │
│ │ - 新增:past:chats 选项(在菜单顶部) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 菜单渲染 │ │
│ │ - showPastChats=false → FileMenu + past:chats 按钮 │ │
│ │ - showPastChats=true → SessionMenu (会话列表) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ bridge.ts │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 新增 API: │ │
│ │ - SearchSessions(query): Promise<SessionMeta[]> │ │
│ │ │ │
│ │ 复用 API: │ │
│ │ - ListSessions(): Promise<SessionMeta[]> │ │
│ │ - PreviewSession(path): Promise<HistoryMessage[]> │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ desktop/app.go │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 新增方法: │ │
│ │ - SearchSessions(query string) []SessionMeta │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 5. UI 设计
### 5.1 @ 菜单(showPastChats=false)
```
┌─────────────────────────────────────────────────────────────┐
│ @ ← 用户输入 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 💬 past:chats 引用历史会话 │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ 📁 src/ │ │
│ │ 📁 docs/ │ │
│ │ 📄 README.md │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [输入消息...] [发送] │
└─────────────────────────────────────────────────────────────┘
```
### 5.2 会话列表(showPastChats=true)
```
┌─────────────────────────────────────────────────────────────┐
│ @past:chats ← 用户输入 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 💬 项目架构设计讨论 - 2026-06-04 │ │
│ │ 💬 数据处理方案 - 2026-06-03 │ │
│ │ 💬 API 接口设计 - 2026-06-02 │ │
│ │ ← 返回文件列表 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ [输入消息...] [发送] │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 引用区域
```
┌─────────────────────────────────────────────────────────────┐
│ 📎 引用的会话: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 📄 项目架构设计讨论 (8 轮) [×] │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ │
│ [输入消息...] [发送] │
└─────────────────────────────────────────────────────────────┘
```
---
## 6. P1 后续优化(暂不实施)
- 选择单条/多条消息
- hover 预览会话详情
- 截断/缓存优化
- 国际化翻译
- 搜索会话功能
---
## 7. 验收标准
### P0 验收
- [ ] 输入 `@` 显示菜单,包含 "past:chats" 选项
- [ ] 选择 "past:chats" 显示历史会话列表
- [ ] 选择会话后显示在引用区域
- [ ] 可以删除引用的会话
- [ ] 发送时正确附加会话上下文
- [ ] 引用内容限制在 30 条消息或 20k 字符内
- [ ] 超出部分截断并提示
### 测试用例
- [ ] 无历史会话时的行为
- [ ] 引用超大会话时的截断
- [ ] 与现有 @ 文件引用同时使用
- [ ] 在不同主题下的显示效果
-531
View File
@@ -1,531 +0,0 @@
# Reasonix Windows SignPath 配置与验收 SOP
本文供 SignPath 管理员、GitHub 仓库管理员和 Release Maintainer 配置并验收
Reasonix Windows Authenticode 两阶段签名链路。
关联变更:
- PR:[esengine/DeepSeek-Reasonix#6904](https://github.com/esengine/DeepSeek-Reasonix/pull/6904)
- 历史 Preview 渠道改造(仅供兼容背景):[esengine/DeepSeek-Reasonix#6155](https://github.com/esengine/DeepSeek-Reasonix/pull/6155)
- 本 SOP 的验收对象:每次执行前通过 PR API 读回的当前 PR Head
- 签名工作流:`.github/workflows/release-stable.yml`、
`.github/workflows/release-desktop.yml`
- 机器契约:`.signpath/contracts/release-signing.yml`
- Authenticode 验证脚本:`scripts/verify-windows-authenticode.ps1`
> 如果 PR Head 已经发生变化,必须重新审查新的 commit 和 workflow diff,
> 不得继续使用本文记录的旧 SHA 进行正式 Secrets 验证。
## 1. 目标与完成标准
本次配置需要完成两阶段 Windows 签名:
1. 使用 `windows-payload` 给安装后实际落盘的 6 个 EXE 签名。
2. 使用 `windows-installer-v2` 验证这 6 个 EXE 已经签名,再给最终 NSIS
安装器签名。
完整放行标准:
- `windows-payload` 和 `windows-installer-v2` 均已导入 SignPath 且状态为
`VALID`。
- 旧的 `windows-installer` 仍然存在并保持 `DEFAULT`,未被覆盖或删除。
- `release-signing` 使用正式证书、Trusted Build System 和 Origin
Verification。
- `release-signing` 保留证书强制要求的 SignPath 审批,但由专用
`CI builds` 账号在对应 GitHub environment 获批后自动完成;发布人只需在
GitHub 批准一次。
- `test-signing-ci-approval` 和 `windows-installer-test-v2` 仅保留给内部签名
验证,不得被公共 Desktop 发布工作流引用。
- AMD64 和 ARM64 均完成正式证书的零发布预检。
- 首次单渠道正式版中,所有 Authenticode 签名均为 `Status = Valid`。
- Windows Defender 环境下安装、启动、更新和卸载均通过。
## 2. 管理员分工
| 角色 | 责任 |
| --- | --- |
| SignPath 组织管理员 / 项目 Configurator | 导入 Artifact Configuration,维护签名策略和 CI User 权限 |
| GitHub 仓库管理员 | 维护 Actions Secrets/Variables,必要时建立官方临时验证分支 |
| Release Maintainer | 批准一次 GitHub `release` environment,发布并验收正式版 |
如果现有维护者没有 SignPath 项目配置权限,组织管理员可以直接执行导入,
或者在项目设置中将维护者或维护者组添加到 `Configurators`。
SignPath 权限说明:
[Users and permissions](https://docs.signpath.io/users/)
## 3. 当前配置基线
截至 2026-07-25 的线上核对结果:
- SignPath 组织:`DeepSeek-Reasonix [OSS]`
- SignPath 项目:`DeepSeek-Reasonix`
- 项目状态:`VALID`
- Repository URL:
`https://github.com/esengine/DeepSeek-Reasonix.git`
- 当前 Artifact Configurations:
- `Initial version`
- `windows-installer`,状态为 `DEFAULT`
- `windows-installer-test-v2`
- `windows-installer-v2`
- `windows-payload`
- `release-signing` 已开启 Trusted Build System 和 Origin Verification。
- `release-signing` 开启 `Use approval process`,Required approvals 为 `1`。
- `release-signing` 的 Allowed build definitions 精确允许:
- `.github/workflows/release-stable.yml`
- `.github/workflows/release-desktop.yml`
- `release-signing` 的 Allowed branches 必须精确为 `main-v2`;正式版标签由
最小 relay workflow 转发到该受保护控制面。
- `test-signing-ci-approval` 使用测试证书,只允许 `CI builds` 提交和审批,
Required approvals 为 `1`,并启用相同的 Trusted Build、Origin 和 Build
Definition 限制。
- `Release certificate 2026` 在证书 Restrictions 中启用了
`Requires approval process`,因此该审批不能在项目策略中关闭。
## 4. 授予 SignPath 配置权限
如果由现有组织管理员亲自导入,可以跳过本节。
1. 登录 SignPath。
2. 进入 `Projects`。
3. 打开 `DeepSeek-Reasonix`。
4. 进入项目编辑或项目权限设置。
5. 在 `Configurators` 中添加负责维护签名配置的用户或用户组。
6. 保存。
7. 重新打开项目。
8. 确认 `Artifact Configurations` 区域出现 `Add` 按钮。
建议授权给维护者组,而不是长期绑定单个账号。
## 5. 导入 `windows-payload`
### 5.1 获取固定版本 XML
必须从经过审查的 PR commit 复制文件,不要手工重新编写 XML:
- 仓库路径:
`.signpath/artifact-configurations/windows-payload.xml`
- 固定版本:
[windows-payload.xml@fe354e5](https://github.com/SivanCola/DeepSeek-Reasonix/blob/fe354e59a9a076930403b7d8aefb0bcd0b4e182a/.signpath/artifact-configurations/windows-payload.xml)
### 5.2 导入步骤
1. SignPath → `Projects` → `DeepSeek-Reasonix`。
2. 找到 `Artifact Configurations`。
3. 点击 `Add`。
4. 选择 `Custom`。
5. 名称填写:
```text
windows-payload
```
6. Slug 必须为:
```text
windows-payload
```
7. 粘贴上述文件的完整 XML。
8. 保存。
9. 确认配置状态为 `VALID`。
10. 点击 `Open XML`,逐字核对线上 XML 与仓库文件一致。
11. 不要将该配置设为 `DEFAULT`。
### 5.3 配置含义
GitHub `upload-artifact` 提交给 SignPath 的产物是 ZIP,因此配置根节点必须是
`<zip-file>`。
该配置需要给以下 6 个 EXE 执行 `authenticode-sign`:
- `reasonix-desktop.exe`
- `reasonix-guard.exe`
- `reasonix-launcher.exe`
- `reasonix-update-helper.exe`
- `reasonix-cli.exe`
- `reasonix-uninstall.exe`
参考:
- [Artifact configurations](https://docs.signpath.io/artifact-configuration/)
- [GitHub trusted build system](https://docs.signpath.io/trusted-build-systems/github)
- [Artifact configuration syntax](https://docs.signpath.io/artifact-configuration/syntax)
## 6. 导入 `windows-installer-v2`
### 6.1 获取固定版本 XML
- 仓库路径:
`.signpath/artifact-configurations/windows-installer-v2.xml`
- 固定版本:
[windows-installer-v2.xml@fe354e5](https://github.com/SivanCola/DeepSeek-Reasonix/blob/fe354e59a9a076930403b7d8aefb0bcd0b4e182a/.signpath/artifact-configurations/windows-installer-v2.xml)
### 6.2 导入步骤
1. 再次点击 `Artifact Configurations → Add → Custom`。
2. 名称填写:
```text
windows-installer-v2
```
3. Slug 必须为:
```text
windows-installer-v2
```
4. 粘贴上述文件的完整 XML。
5. 保存。
6. 确认配置状态为 `VALID`。
7. 点击 `Open XML`,逐字核对线上 XML 与仓库文件一致。
8. 不要将该配置设为 `DEFAULT`。
### 6.3 配置含义
该配置需要:
1. 对最终的 `*installer*.exe` 执行 `authenticode-sign`。
2. 对上述 6 个内层 EXE 执行 `authenticode-verify`。
如果任一内层 EXE 未签名,或签名后又被修改,第二阶段请求必须失败,不能继续
生成可发布的安装器。
参考:
- [Artifact configuration reference](https://docs.signpath.io/artifact-configuration/reference)
- [Projects and versioned configurations](https://docs.signpath.io/projects)
## 7. 保留旧配置
导入完成后的 Artifact Configurations 应为:
| 配置 | 预期状态 |
| --- | --- |
| `Initial version` | 保留 |
| `windows-installer` | 保留并继续为 `DEFAULT` |
| `windows-payload` | 新增、`VALID`、非 `DEFAULT` |
| `windows-installer-v2` | 新增、`VALID`、非 `DEFAULT` |
禁止执行以下操作:
- 删除 `windows-installer`。
- 将 `windows-installer` 的 XML 替换为新配置。
- 修改旧配置的 Slug。
- 将两个新配置设置为 DEFAULT。
新工作流通过明确的
`artifact-configuration-slug` 选择配置,不需要改变默认配置。保留旧配置是为了
保证旧 release ref 仍然可重跑。
## 8. 检查并修正签名策略
### 8.1 `test-signing`
打开 `test-signing` 并确认:
- 使用测试证书。
- Submitters 包含 `CI builds`。
- CI 请求可以自动完成,不要求人工审批。
- 如果启用 Origin Verification,仓库地址必须为:
```text
https://github.com/esengine/DeepSeek-Reasonix.git
```
### 8.2 `release-signing`
打开 `release-signing → Edit` 并设置:
- Purpose:`Release signing`
- Certificate:正式 Release certificate
- Submitters:必须包含 `CI builds`
- `Require trusted build system`:开启
- `Verify origin policy`:开启
- Repository URL:
```text
https://github.com/esengine/DeepSeek-Reasonix.git
```
- Allowed branches:**只能填写 `main-v2`**
- Allowed build definitions:**只能逐行填写以下两个精确路径**:
```text
.github/workflows/release-stable.yml
.github/workflows/release-desktop.yml
```
不得使用 `.github/workflows/release-*.yml` 通配符,也不得加入只负责转发
dispatch 的 trigger workflow。仓库内
`.signpath/contracts/release-signing.yml` 是该列表的机器可读事实源;CI 会
解析 workflow 调用图,发现新的顶层签名入口时失败关闭。
- `Use approval process`:**保持开启**
- Required approvals:`1`
- Approvers:必须至少包含能够处理正式发布的 SignPath 人工审批人
保存后重新打开策略,确认:
- 策略状态为 `VALID`。
- `Use approval process` 已开启。
- Trusted Build System 和 Origin Verification 仍然开启。
- Allowed branches 精确显示 `main-v2`,没有 `**`、`v*`、
`desktop-v*` 或临时测试分支。
- Allowed build definitions 与仓库机器契约逐项相同,没有通配符。
正式版的 `vX.Y.Z` 标签事件由 `release-stable-trigger.yml` 转发:relay
只携带候选 tag,实际顶层发布 workflow 固定运行在受保护的 `main-v2`,再签署
`vX.Y.Z`、`npm-vX.Y.Z` 和 `desktop-vX.Y.Z` 共同指向的不可变候选 SHA。
不能把 Allowed branches 改成标签通配符,因为普通分支也可以取形如
`v-malicious` 的名字。
`Release certificate 2026` 的 Restrictions 明确要求所有使用该证书的策略启用
审批流程。尝试关闭时,SignPath 会拒绝保存并提示:
```text
Certificate requires an approval process.
You can either enable the approval process or use another certificate.
```
因此不得关闭正式签名策略的审批。工作流先以
`wait-for-completion: false` 提交请求,取得 Signing Request ID,再由专用
`CI builds` 账号调用 SignPath `Approve` API,并轮询、下载签名产物。正式版
使用 `release-signing`;测试证书仅用于不发布产物的独立内部验证。
## 9. 检查 GitHub Actions 配置
进入:
`Settings → Secrets and variables → Actions`
确认以下 Repository Secrets 存在:
- `SIGNPATH_API_TOKEN`
- `SIGNPATH_ORGANIZATION_ID`
安全要求:
- 不得在日志、截图、Issue、PR 评论或聊天中显示 Secret 值。
- Token 对应的 SignPath CI User 应为 `CI builds`。
- `CI builds` 必须具备 `release-signing` 的 Submitter、Approver 权限。
只有独立内部签名验证仍在使用测试策略时,才额外授予
`test-signing-ci-approval` 权限。
- 不得把个人 Interactive User 的 Token 用作 `SIGNPATH_API_TOKEN`。
- `SIGNPATH_ORGANIZATION_ID` 必须指向正确的 OSS 组织。
- GitHub `release` environment 的审批人仍然有效。
- `release-signing` 的 Allowed branches 精确为 `main-v2`。
正式验收前,应使签名契约 attestation 失效:
```bash
gh variable set SIGNPATH_RELEASE_SIGNING_ATTESTATION \
--repo esengine/DeepSeek-Reasonix \
--body unverified
```
这会使任何依赖旧 attestation 的 standalone 历史路径失败关闭。正式版不读取
旧 attestation 放行,而是在同一次获批运行中先完成真实签名预检,成功后才
启动 CLI、npm 和 Desktop 发布。
## 10. 运行 AMD64/ARM64 正式证书零发布预检
Fork PR 工作流拿不到官方仓库的 SignPath Secrets,因此不能直接在 PR 分支
完成真实签名。不要为了合并前验证而放宽 `release-signing` 的精确
`main-v2` 分支限制。代码、workflow 契约和无 Secrets 的打包测试在 PR 中
通过后,合并到受保护的 `main-v2`,再执行正式证书预检。
零发布预检由 `.github/workflows/release-stable.yml` 在唯一的 `release`
environment 获批后自动调用。它使用 `release-signing` 和正式证书验证
AMD64/ARM64,由 `CI builds` 自动批准 SignPath 请求,并跳过 publish job。
四个请求全部成功后,当前契约指纹会写入
`SIGNPATH_RELEASE_SIGNING_ATTESTATION`。
```bash
./scripts/release-stable.sh X.Y.Z
```
该命令先验证远端 `main-v2` 的 reviewed Notes 和精确 SHA CI,再原子创建三个
正式版标签;随后 relay 启动受保护控制面。预检完成前,CLI、npm 和 Desktop
三个公开 publisher 均不会启动,因此 SignPath 策略漂移不会形成半发布。
不要为了预检重新引入 Preview/RC 标签或 `canary` environment。
### 10.1 监控运行
```bash
RUN_ID="$(gh run list \
--repo esengine/DeepSeek-Reasonix \
--workflow release-stable.yml \
--branch main-v2 \
--event workflow_dispatch \
--limit 1 \
--json databaseId \
--jq '.[0].databaseId')"
gh run watch "$RUN_ID" \
--repo esengine/DeepSeek-Reasonix \
--exit-status
```
## 11. 正式证书预检验收标准
以下两个任务必须同时成功:
- `build (windows-amd64)`
- `build (windows-arm64)`
每个平台必须完成:
1. 构建未签名 payload。
2. 上传 payload。
3. 使用 `windows-payload` 签署 6 个 EXE;`CI builds` 自动记录审批。
4. 使用已签 payload 重新生成 portable ZIP 和 NSIS 安装器。
5. 上传 installer signing bundle。
6. 使用 `windows-installer-v2` 验证内层可信签名并签署外层安装器。
7. 执行 Authenticode release contract 验证。
8. `publish` job 因 `signing_preflight=true` 被跳过。
SignPath Signing Requests 中应出现 4 个成功请求:
| 架构 | 第一阶段 | 第二阶段 |
| --- | --- | --- |
| AMD64 | payload 签名 | installer 验证及签名 |
| ARM64 | payload 签名 | installer 验证及签名 |
逐个检查:
- 状态为 `Completed`。
- Artifact Configuration Slug 正确。
- Origin 指向官方仓库。
- Commit SHA 与 GitHub Actions 运行 SHA 一致。
- Trusted Build、Origin Verification、Malware Scan 均通过。
- 自动预检和正常发布请求的批准 Actor 均为 `CI builds`。
- AMD64 和 ARM64 的 payload、portable ZIP 内文件及最终 installer 均通过
`Status = Valid` 信任链验证。
## 12. 核对正式签名 attestation
只有以下条件全部满足后才能开启:
- 两个新 Artifact Configuration 均为 `VALID`。
- 旧 `windows-installer` 未改变。
- `release-signing` 的证书级审批保持开启,正式审批人可用。
- `release-signing` 的 Build Definitions 精确允许
`.github/workflows/release-stable.yml`、
`.github/workflows/release-desktop.yml`。
- `release-signing` 的 Allowed branches 精确为 `main-v2`。
- `CI builds` 是 `release-signing` 的 Submitter 和 Approver,GitHub Secret 使用其专用
Token。
- AMD64 和 ARM64 正式证书零发布预检全部成功。
- 4 个 SignPath Signing Request 全部成功。
预检会自动写入变量,只需读回核对:
```bash
gh variable get SIGNPATH_RELEASE_SIGNING_ATTESTATION \
--repo esengine/DeepSeek-Reasonix
```
值必须为 `v1:` 加 64 位小写十六进制 SHA-256。只要 workflow、签名脚本、
Artifact Configuration 或机器契约改变,CI 计算出的新指纹就不再匹配,必须
重新运行零发布预检。
## 13. 首次单渠道正式版验收
单渠道发布不再创建公开 RC。`signing_preflight=true` 在唯一的正式版 run 内
证明正式证书、双阶段产物链和自动审批闭环正确,成功后才允许同一 run 的公开
publisher 启动。首次切换后的版本必须完整执行本文第 11、12、14 节以及仓库
`docs/RELEASING.md` 的跨表面 postflight;不得用额外 prerelease 替代。
## 14. 正式证书与 Defender 验收
正式版 Windows 验证会启用 `RequireTrusted=true`。所有 Authenticode 签名必须
返回:
```text
Status = Valid
```
在干净的 Windows 11 AMD64 和 ARM64 环境中检查安装目录:
```powershell
Get-ChildItem "<Reasonix安装目录>" -Recurse -Filter *.exe |
ForEach-Object {
$signature = Get-AuthenticodeSignature $_.FullName
[PSCustomObject]@{
File = $_.FullName
Status = $signature.Status
Subject = $signature.SignerCertificate.Subject
}
}
```
验收要求:
- 安装器签名为 `Valid`。
- 安装目录内全部 6 个 EXE 签名为 `Valid`。
- Portable ZIP 内的可执行文件签名为 `Valid`。
- AMD64 和 ARM64 的签名证书 Subject 符合预期。
- Windows Defender 保持开启。
- 实际完成安装、首次启动、CLI 调用、更新和卸载。
- Windows Security → Protection History 中没有新隔离或拦截。
只有 AMD64 和 ARM64 都通过,首次单渠道正式版才算完成验收。
## 15. 失败处理与回滚
| 现象 | 处理 |
| --- | --- |
| 看不到 `Add` | 添加项目 Configurator,或由组织管理员直接导入 |
| XML 无法保存或状态不是 `VALID` | 只修复新配置,不修改旧 `windows-installer` |
| 内部 Test Signing Request 长时间 Pending | 检查测试策略审批配置、CI User、配置 Slug 和请求错误;公共 workflow 不得切换到测试策略绕过问题 |
| Release Signing Request 显示 Pending approval | 这是正式证书的强制门禁;由授权 SignPath 审批人在 Action 超时前处理 |
| Origin Verification 失败 | 核对仓库 URL、ref、SHA 和 GitHub Trusted Build |
| 提示文件缺失或存在额外文件 | 检查 signing bundle 与 XML 文件清单是否一致 |
| `authenticode-verify` 失败 | 检查内层文件是否未签名,或签名后被重新编译/修改 |
| 只有 AMD64 成功 | 不放行,ARM64 也是硬门槛 |
| 正式版签名不是 `Status = Valid` | 将 attestation 设为 `unverified`,停止后续发布并按不可变标签恢复缺失表面 |
| 自动预检请求等待人工审批 | 将 attestation 设为 `unverified`,检查 `CI builds` Approver 权限、API Token 和自动审批步骤;不得关闭证书强制审批 |
发生正式签名故障时,立即恢复失败关闭:
```bash
gh variable set SIGNPATH_RELEASE_SIGNING_ATTESTATION \
--repo esengine/DeepSeek-Reasonix \
--body unverified
```
在故障解除并重新完成 AMD64/ARM64 验收前,不得发布稳定版。
## 16. 最终签字清单
- [ ] `windows-payload` 已导入且为 `VALID`
- [ ] `windows-installer-v2` 已导入且为 `VALID`
- [ ] 旧 `windows-installer` 仍存在并保持 `DEFAULT`
- [ ] 公共发布 workflow 未引用 `test-signing-ci-approval` 或 `windows-installer-test-v2`
- [ ] `CI builds` 是 `release-signing` 的 Submitter、Approver
- [ ] `SIGNPATH_API_TOKEN` 对应专用 `CI builds`,不是个人账号
- [ ] `release-signing` 已开启 Trusted Build System
- [ ] `release-signing` 已开启 Origin Verification
- [ ] `release-signing` 的 Allowed build definitions 精确为 `.github/workflows/release-stable.yml` 和 `.github/workflows/release-desktop.yml`
- [ ] `release-signing` 的 Allowed branches 精确为 `main-v2`
- [ ] `release-signing` 的 SignPath 审批已开启,Required approvals 为 `1`
- [ ] GitHub `release` environment 的正式发布审批人和响应流程已经明确
- [ ] AMD64 正式证书零发布预检两阶段签名成功
- [ ] ARM64 正式证书零发布预检两阶段签名成功
- [ ] 4 个 SignPath Signing Request 均为 `Completed`
- [ ] `SIGNPATH_RELEASE_SIGNING_ATTESTATION` 与当前机器契约指纹一致
- [ ] 首次单渠道正式版的 AMD64 签名均为 `Valid`
- [ ] 首次单渠道正式版的 ARM64 签名均为 `Valid`
- [ ] Defender 安装、启动、更新和卸载验证通过
- [ ] 签名稳定版已真实可下载后,再关闭对应问题单
## 17. 参考资料
- [SignPath Artifact Configuration](https://docs.signpath.io/artifact-configuration/)
- [SignPath Artifact Configuration Syntax](https://docs.signpath.io/artifact-configuration/syntax)
- [SignPath Artifact Configuration Reference](https://docs.signpath.io/artifact-configuration/reference)
- [SignPath Projects](https://docs.signpath.io/projects)
- [SignPath Users and Permissions](https://docs.signpath.io/users/)
- [SignPath GitHub Trusted Build System](https://docs.signpath.io/trusted-build-systems/github)
- [Reasonix PR #6904](https://github.com/esengine/DeepSeek-Reasonix/pull/6904)
-492
View File
@@ -1,492 +0,0 @@
# Reasonix 工程规格
<a href="./SPEC.md">English</a>
> Reasonix 是一个 coding agent:由极薄的 harness 驱动多个模型,所有能力都由配置和插件提供。本文是工程契约,代码应遵循它;需要改变行为时,应先更新契约,再修改代码。
英文原文是规范性版本;本文按相同章节提供中文说明,代码标识符、配置键和协议名保持原样。
## 1. 设计原则
1. **配置与插件驱动。** 核心只依赖接口;具体模型和工具通过 registry 按名称解析、在配置中声明,或由插件注入,不硬编码 `switch model`。
2. **单一静态二进制。** 使用 `CGO_ENABLED=0`,一条命令完成跨平台编译,CLI 开箱即用。
3. **精简依赖。** 默认使用标准库。第三方依赖必须是纯 Go、足够轻量,且不能破坏单二进制、跨平台和分发体验;TOML parser 是当前唯一接受的基础依赖。
4. **两级扩展。** 编译期 built-in 通过 `init()` 自注册;运行时外部插件以 stdio JSON-RPC 子进程或 MCP 兼容传输接入。
5. **接口优先、registry 驱动。** `Provider` 与 `Tool` 都是接口。
6. **持续演进,不过度设计。**
所有代码、注释、面向用户的字符串、工具描述、system prompt 和英文规范以英语为主;README 同时维护英文版 `README.md` 与中文版 `README.zh-CN.md`。
## 2. 目录与依赖方向
```text
reasonix/
├── go.mod / go.sum
├── Makefile
├── README.md / README.zh-CN.md
├── reasonix.example.toml
├── docs/SPEC.md / docs/SPEC.zh-CN.md
├── cmd/reasonix/main.go
├── cmd/reasonix-plugin-example/
└── internal/
├── cli/
├── config/
├── provider/
│ └── openai/
├── tool/
│ └── builtin/
├── permission/
├── command/
├── plugin/
├── remote/
│ ├── forward/
│ ├── sftpfs/
│ └── bootstrap/
└── agent/
```
核心依赖方向保持无环:
```text
cli → {agent, plugin, config} → {tool, provider}
```
`provider/openai`、`tool/builtin` 等 built-in 子包导入父包完成自注册,父包不反向导入子包。Remote-SSH 采用 `cli → remote/bootstrap → remote` 的分层,`remote` 及其子包不依赖 `cli`、`agent` 或 `serve`;host key 和 secret prompt 等交互都通过 callback 暴露,供桌面端复用。
## 3. 核心抽象
### 3.1 Provider 与 registry(`internal/provider`)
```go
type Provider interface {
Name() string
Stream(ctx context.Context, req Request) (<-chan Chunk, error)
}
type Factory func(cfg Config) (Provider, error)
func Register(kind string, f Factory)
func New(kind string, cfg Config) (Provider, error)
```
- `openai` kind 实现 OpenAI-compatible `/chat/completions`。
- OpenAI-compatible vendor 只是 `kind = "openai"` 的不同配置实例,通过 `base_url`、`model`、`api_key_env` 区分;新增兼容模型通常只需改配置。
- 一个 provider 表示一个 vendor endpoint,可通过 `models` 暴露多个模型,并以 `default` 指定默认项。设置 `request_url` 时,OpenAI-compatible、Anthropic-compatible 和 Responses provider 都会原样使用该完整请求地址;旧 `chat_url` 只保留 OpenAI 历史兼容语义。`default_model`、`--model` 和桌面端模型选择器都经 `Config.ResolveModel` 解析,可接受 provider 名、裸模型名或 `provider/model`。
- `context_window` 是 provider 级默认值;`model_overrides.<model>.context_window` 可覆盖单个模型。
- `max_output_tokens` 是独立的本轮输出上限,不由客户端 reasoning 字节上限换算,也不参与 `compact_ratio`。推荐 `0`(自动:DeepSeek 默认 high 约 64K);显式 `32768` 控费/普通编码,`65536` 重推理/长工具链,`131072` 仅在反复 `finish_reason=length` 时再考虑。正数为显式上限,负数为在协议允许时省略;混合网关可用 `model_overrides.<model>.max_output_tokens` 覆盖单个模型。Anthropic 因协议要求仍会提供 `max_tokens` 默认值。
- streaming tool-call delta 在 provider 内按 index 聚合,只向上层发出完整 `ToolCall`。
### 3.2 Tool 与 registry(`internal/tool`)
```go
type Tool interface {
Name() string
Description() string
Schema() json.RawMessage
Execute(ctx context.Context, args json.RawMessage) (string, error)
}
```
- built-in tool 通过 `tool.RegisterBuiltin` 注册到进程级集合。
- 每次运行创建独立 `*Registry`,由启用的 built-in 与插件工具组成;agent 只看到该 registry。
- tool schema 在插入 registry 时 canonicalize;内置契约见[工具合约](./TOOL_CONTRACT.zh-CN.md),测试会校验文档与 canonical schema 不漂移。
- `Execute` 自行解析原始 JSON 参数。错误作为结果返回给模型,让模型有机会自我修正,而不是直接终止进程。
### 3.3 插件与 MCP(`internal/plugin`)
外部插件是配置中声明的 MCP server。协议统一为 JSON-RPC 2.0,传输由 `transport` 接口抽象:
- `stdio`:本地持久子进程,每行一条 JSON 消息。
- `http` / `streamable-http`:向远程 `url` POST,支持 `application/json` 和 SSE 响应,并复用 `Mcp-Session-Id`。未配置静态 `Authorization` header 时,用户可发起 OAuth:客户端按 Protected Resource Metadata / Authorization Server Metadata 发现端点,使用动态客户端注册、PKCE S256、loopback callback、resource indicator 与 refresh token 轮换。客户端凭据和 token 以 `0600` 权限保存在工作区之外的 Reasonix 私有 MCP 状态目录,并绑定到配置的 resource URL;URL 改变后不会复用旧 token。OAuth 发现、注册和 token 请求遵守 Reasonix 解析后的网络代理设置。删除声明时会清理该状态;若之后生效的 fallback 使用同一 OAuth resource,则保留该状态。
- `sse`:兼容旧版 2024-11-05 HTTP+SSE;持久 GET 接收 server 公布的相对 POST endpoint、JSON-RPC 响应与 server 消息。为避免静态 header 泄漏,会拒绝跨域 endpoint。
`${VAR}` 与 `${VAR:-default}` 可用于 `command`、`args`、`env`、`url` 和 `headers`,使 secret 留在环境中。生命周期为 `initialize` → `notifications/initialized` → `tools/list`,调用使用 `tools/call`。
存在工作区根目录时,初始化会声明 `roots` 能力,并用文件 URI 响应 `roots/list`。`tools/call` 会附带逐调用 `_meta.progressToken`;匹配的 `notifications/progress` 会进入现有工具进度事件链路。
远程工具适配为 `Tool`,命名为 `mcp__<server>__<tool>`。`annotations.readOnlyHint` 映射为 `Tool.ReadOnly()`,默认 false;只有显式声明为只读的工具才进入并行读取与默认只读权限路径。MCP prompt 暴露为 slash command,resource 可通过 `@<server>:<uri>` 引用。
### 3.4 Agent loop(`internal/agent`)
`Session` 保存 `[]Message`。`Run(ctx, input)` 的主循环为:
1. 构建包含历史消息和 tool schema 的 `Request`。
2. 调用 `provider.Stream` 并实时输出 text delta。
3. 收集完整 tool call;若没有 tool call,则本回合结束。
4. 执行 built-in 或 plugin tool,把结果加入会话后继续,直到完成或达到安全边界。
`ctx` 贯穿调用链,Ctrl-C 可以取消进行中的请求。`Agent` 与 `Coordinator` 都实现 `Runner`,因此 CLI 不需要区分单模型或双模型执行。
### 3.5 双模型协作(`Coordinator`)
当 `agent.planner_model` 与 executor 不同时,planner 与 executor 使用独立 session:
- 宿主使用原始用户文本和可信回合元数据做确定性路由,不调用 classifier 模型,也不从
controller 注入的 prompt block 猜测宿主状态;路由结果为 executor-only、Light、Full、
plan-for-approval 或显式 plan-only,并用不含用户原文的 route/depth/reason 写入阶段详情;
- 显式 Plan Mode、synthetic turn、上下文短回复、明确单点小改和边界清楚的纯只读动作
不再调用第二个 Planner;跨面、结构化、模糊或高风险工作使用 Full;活跃 Goal 与
Delivery 中的非原子修改工作同样升级为 Full,纯只读动作仍直达 Executor;
- Light 使用较小的单轮调研预算,输出紧凑目标、1–4 个有序步骤、候选触点和主要验证;
Full 使用较大的有界预算,区分已验证与候选触点,并补充风险、验收标准、命令级验证及
必要回滚;深度合约保持在同一个稳定 system prompt 中,单轮只追加很小的
`<planner-turn>`;若 Planner 在有界调研和最终总结轮后仍未收敛,普通
plan-and-execute 用原始任务降级到 Executor,plan-only 与 plan-for-approval 仍保持
fail-closed;不完整的 Planner 回合会被回滚,不暴露成无法继续的手动续跑;
- 普通“先规划”在计划完成后直接交接 Executor;plan-for-approval 只用于明确要求等待
确认的请求,由宿主强制审批边界,批准后交接 Executor;headless 场景会保存计划供后续
回合继续;明确 plan-only 会保存计划并结束当前回合;上述两种执行边界下 Planner 失败
都不能降级执行;这些边界可位于任务子句之后,引号内的示例不改变路由;
- executor 在另一 session 中验证候选假设,并使用完整工具执行计划;
- 两条会话互不混合,prompt prefix 都只追加增长,避免切换模型破坏 prefix cache。
### 3.6 上下文管理(内容驱动摘要)
长任务会填满模型窗口。Reasonix 保持 **cache-first、append-only** 的 canonical
transcript,仅在唯一自动阈值被跨越时安装 provider 可见的短 **checkpoint**。
- 每个 provider 声明 `context_window`(tokens)。唯一自动触发值是
`agent.compact_ratio`(默认 **0.85**;预设 0.70 / 0.80 / 0.85;范围 0.65–0.85)。
`triggerTokens = floor(context_window × compact_ratio)`。
- **阈值以下**绝不改写历史:不摘要、不安装 prune/snip projection、不写 sidecar、
不增加 projection version、不发维护事件。任何改写都会使该点之后的 prompt 缓存失效。
- **达到阈值**时运行 **一次** 摘要事务:
`稳定前缀 + 一个结构化摘要 + 最近原文尾部`。
正常验收:候选 ≤ 窗口 50%、严格小于源、且低于 `triggerTokens`;**不会**向 50% 回填。
典型落地约占窗口 10%–30%。
内部构造预算(非用户设置):
`recentTailBudget = clamp(window×10%, 32K, 96K)`,摘要输出上限 **16K**。
- **用户轮次不交给摘要器裁决**:折叠区内的每条 user turn 在预算内原样保留(单条
≤1500 tokens,合计 `min(8192, window×5%)`,从最旧开始)。理由是丢失的不对称
性——第 4 轮说的"不许改 public API"只存在于 transcript 里,它约束的代码却能从
工作区重新推导。预算是必须的:无上限地保留会把候选撑过验收天花板,使压缩直接
失败而非降级。该保护不以最近一次 digest 为界,因此能跨多次压缩存活;超出预算的
轮次可用 `[[keep]]` 前缀(keep 策略 `user_marked`,默认开启)强制原样保留。
丢弃不是静默的:压缩 telemetry 带 `user_kept` / `user_dropped` 计数,且已提交的
checkpoint 若折叠了用户轮次会发出提示 `[[keep]]` 的警告——两种情况下 projection
读起来都是完整的,计数是唯一能区分它们的东西。
- **失败保护必须跨多次折叠成立**:`KeepErrors` 依据宿主的 `ToolExecution` 记录而非
文本判定失败(真实 `go test` 日志以 `=== RUN` 开头,前缀匹配看不见它),因此存储的
projection 保留该记录,而发往 provider 的请求不带。剥离发生在 provider 边界
(`ModelMessages`),projection 写入用 `ProjectionMessages` 保留——否则下一次折叠
将无法分类上一次刚刚保护下来的失败。
- 用户可用 `reasonix config compact-ratio [--local] [VALUE]` 查看或修改阈值。
项目配置优先于桌面与新 CLI 会话共用的用户全局配置。UI 始终展示**实际生效**值。
- `max_output_tokens` 是独立的**本轮**输出上限。
推荐 `0`(**自动**,不是无限;DeepSeek 默认 high → 约 64K)。
用户侧常用值:`32768` 普通编码/控费,`65536` 重推理/长工具链,`131072` 仅在反复
`finish_reason=length` 时再考虑。负数为在协议允许时省略。仅在发送阶段按剩余
窗口裁剪,**绝不**改变 `triggerTokens` 或维护时机。计费按实际 completion,不按配置上限。
- 巨型工具结果只在**第一次**进入模型前限长:`Content` 为稳定 ≤32KB 可见版;
超限时 `RawContent` 保存完整原文。后续维护不得回头改写。`ModelMessages` 会去掉
`RawContent`,provider 序列化与缓存 hash 永不包含它。
- 自动维护只在 `ContextManager.Prepare` 中规划一次,输入为当前 projection 加上
append-only canonical tail;canonical 永不改写。后续阈值合并
**上一摘要 + 新增历史** 为单条 digest(无 multi-span、无应用层重试)。
失败以 generation 为边界记录 `blocked`/`failed`,同 generation 不自动再付费;
手动 `compress` 可重试。
- 旧多阈值键(`soft_compact_ratio`、`tool_result_snip_ratio`、
`compact_force_ratio`、`cold_resume_prune`、`context_editing`)在普通启动时删除,
运行时忽略。不再使用 provider 原生 tool clearing;所有 provider 走本地 summary
checkpoint。
- `keep` / `recent_keep` 与活跃工具轮次仍受保护。重启只恢复既有 checkpoint,
不重新摘要、不重放时间线卡片。
- 完整历史保留在会话 transcript 中;`history` tool 提供 BM25 检索。新 checkpoint
不再创建 prune archive。
`history` tool 支持对 session 与归档进行 BM25 搜索;`memory` tool 用于检索自动记忆,
`remember` 与 `forget` 负责写入和归档。每个真实用户回合前,Reasonix 会用原始用户消息执行
有预算的 BM25 自动召回,把命中作为低权限 user-turn 后缀追加;泛化请求会被抑制,等价事实优先
项目级版本,stale 内容会降权。这不会修改稳定 system prompt 或工具 schema。
拥有当前项目 store 的父 controller(包括顶层 headless)只有在新事实有界、非敏感、纯创建,且明确属于 project/reference 时才能
免确认保存。全局事实、偏好、feedback、更新、重复项、敏感/超长内容和所有 `forget` 仍需
新鲜人工确认,Auto、YOLO、Guardian、permission hook 或子智能体都不能代为批准;子智能体和
不拥有该作用域 controller 的 headless surface 会 fail closed。事实带有不变 ID、单调 revision、时间、type 与 scope;更新先快照旧版本,
restore 与 archive recovery 会创建更高 revision,并拒绝路径逃逸、符号链接、冲突和覆盖。
详细约定见 [`SESSION_MEMORY_RETRIEVAL.zh-CN.md`](SESSION_MEMORY_RETRIEVAL.zh-CN.md)。
### 3.7 权限
权限层按单次 tool call 返回 `Allow`、`Ask` 或 `Deny`:
```go
type Decision int
const (Allow Decision = iota; Ask; Deny)
type Policy struct { Mode Decision; Allow, Ask, Deny []Rule }
func (p Policy) Decide(toolName string, readOnly bool, args json.RawMessage) Decision
```
- rule 可以是 `Tool` 或 `Tool(specifier)`,例如 `Bash(go test:*)`、`Edit(docs/**)`;`Bash=<literal>` 是整条 Bash 命令的精确授权格式,其中 glob 与 Shell 元字符都按普通字符匹配。
- 优先级为 `deny > ask > allow > fallback`;只读工具 fallback 为 Allow,写工具 fallback 使用 `Mode`。
- 交互模式中的 Ask 由用户选择单次允许、session scope 允许、持久允许或拒绝;显式 Deny 在所有模式下都不可绕过。
- 非交互 `reasonix run` 与无头子智能体没有审批界面:默认 Ask/manual 对普通 writer fallback 与显式 ask 规则失败关闭;Auto 只放行普通 writer fallback,显式 ask 仍拒绝;YOLO 可越过普通 Ask,但不能越过 deny、Sandbox 或强制新鲜人工审批。无人值守自动化需要普通 writer 自主执行时,使用现有的 `--auto` / `-y`。
- 动态 Bash 分两级:参数/算术展开、赋值、不含嵌套执行的 heredoc、普通文件重定向与 Shell glob 不能复用裸 `Bash`、前缀或 glob Allow,保存时只生成 `Bash=<literal>`,但仍遵循普通 fallback,因此 Auto 与获批计划窗口可无提示执行。命令/进程替换、动态命令名、无法解析结构,以及 `eval`、`source`、Shell `-c`、PowerShell/cmd 命令字符串、运行时内联代码参数属于嵌套/间接执行;默认情况下交互 Ask/Auto 必须人工批准,Guardian 与 hook allow 不能代替,无头 Ask/Auto/DontAsk 直接拒绝,只有完全相同的 literal 或 YOLO 可以绕过。高级用户可设置 `[permissions] allow_dynamic_bash = true`,让 Allow fallback(包括 Auto)覆盖这类动态命令;显式 `ask` 与 `deny` 规则仍然优先。
- 安装 MCP server 即授权其全部工具,不再有 server、raw tool、writer 或 destructive 的第二套审批策略;项目 `reasonix.toml` 与 `.mcp.json` 声明同样默认可信,不需要额外启动确认,显式全局 `deny` 仍然优先。全局安装写入用户 `config.toml`,项目声明保留在原项目文件;同名时项目覆盖全局,项目内部 `reasonix.toml` 高于 `.mcp.json`。编辑写回当前生效来源,删除高优先级声明后露出下一层。`readOnlyHint` 与 `destructiveHint` 仅用于调度、Plan/严格只读边界及缓存到实时安全分类复核,不会新增逐调用审批。严格只读子智能体 registry 仍仅暴露已授权且 `readOnlyHint: true`、无 `destructiveHint` 的 MCP;双模型 Planner 通过固定 `use_capability` 代理(从不暴露直接 `mcp__*` schema)调用已授权、非 destructive 的 MCP,不再要求 `readOnlyHint`,destructive 工具留给 Executor。Balanced 双模型的 Executor 使用独立 frontend 复用同一稳定代理,因此 Planner 发现的 capability ID 可在 handoff 后直接执行,同时保持两侧 ledger/audit 隔离。分发前代理会再次复核当前 controller 的 enable、授权和完整运行时连接身份;共享 Host 中仅 server 同名不构成复用权限。
- Plan 是协作流程,不等于全工具只读。普通 built-in 与 Bash 仍走 Ask/Auto/YOLO 和 Sandbox;独立双模型 Planner 允许已授权、非 destructive 的 MCP(即使没有 `readOnlyHint`),但在规划阶段持续阻止 destructive 与未授权目标;没有独立 Planner 的单模型 Plan 仍阻止 MCP writer/destructive。
- Plan 只能由用户显式选择进入,与当前工具审批姿态相互独立;普通聊天不会自动切换到 Plan。Auto/YOLO 不会回答 `ask`,也不会替用户批准 `exit_plan_mode`,获批计划的短期自动执行窗口也不会自动批准后续计划或嵌套/间接 Bash。
- 桌面端协作模式分为 `normal`、`plan` 和 `goal`。Goal 默认不设模型轮数、跨 Run turn 数、墙钟时长或数字式无进展边界,会持续推进直到完成、真实用户/外部阻塞、用户停止/暂停、不可恢复外部错误或用户显式预算耗尽。相同宿主失败、零新增证据与 Todo 停滞阈值只触发重新规划,不产生 `goal_run_budget` 或 `goal_stuck` 暂停。Goal 范围的新颖证据允许新的读取/搜索结果推进任务,但拒绝完全相同的工具、参数和结果重复。未配置相应预算时,累计 turn、token、真实 provider 请求数和实际工作时间只做观测。正数 `[agent].goal_token_budget`、`max_steps`、时间或成本预算仍是用户可选的可恢复边界;Goal token 预算默认 `0`(关闭),从 `budget_spend` 恢复会授予新的预算切片且不清零累计统计;`task_time_budget_minutes = 0`(以及兼容的负数)表示关闭时间边界。旧简单/写入/研究参数仅为兼容元数据,所有目标共用同一个 Goal FSM、宿主 receipt、Delivery readiness 和有界 evaluator。普通聊天不会隐式切换协作模式;旧 `.reasonix/autoresearch/.../` 目录只读,显式旧路径可恢复为普通 Goal。
### 3.8 Slash command
Slash command 分为三类:
- built-in action:`/compact`、`/new`、`/clear`、`/effort`、`/mcp`、`/help`;
- `.reasonix/commands/*.md` 与用户配置目录中的自定义命令;
- MCP prompt:`/mcp__<server>__<prompt>`。
自定义命令支持简单 frontmatter、`$ARGUMENTS`、`$1…$N` 和 `$$`。加载失败的单个命令会被跳过,不应使应用整体退出。
Bubble Tea TUI 的 modal overlay 必须隐藏 composer;slash/`@` autocomplete 等 input-owned overlay 保留 composer。新增 overlay 时必须更新 `chat_tui.hideComposer()` 与 layout test。
### 3.9 `@` 引用
- `@<server>:<uri>` 读取 MCP resource;
- `@<path>` 仅在本地路径真实存在时读取文件或目录,普通 `@mention` 与邮箱保持原文本;
- 文件内容有大小限制,binary 只标记不展开;目录按深度优先列出并跳过 `.git`、`node_modules` 等噪音;
- 解析异步进行,失败显示 notice 但不阻止本回合;
- autocomplete 每次只读取一层目录,避免在大型目录中递归遍历。
### 3.10 子智能体 Profile
子智能体 Profile 是带 `runAs: subagent` 的 Skill。桌面端和 CLI 只允许修改简单、手动调用的 project/global profile;包含 `references/`、`scripts/` 或非托管 frontmatter 的丰富 Skill 不会被编辑器扁平化覆盖。
`reasonix subagent try` 使用只读 Skill runner;`reasonix subagent run` 使用常规权限与 Sandbox。`task` 支持 `profile`、`model`、`effort` 和 `write_paths`;`fleet` 在 session scheduler 上并发调度多个任务。详见[子智能体 Profile](./SUBAGENT_PROFILES.zh-CN.md)。
Profile 描述的是 worker,不是一次运行。委派由五个彼此独立的概念构成:profile 说明这个 worker 怎么思考,`TaskSpec` 说明本次要什么,`CapabilityGrant` 说明本次能碰什么,`ContextCapsule` 说明从什么上下文起步,`SchedulerPolicy` 说明何时以及怎么运行。字段归属于**决定其取值**的那一方,因此 profile 可以携带能力**上界**(`allowed-tools`、`read-only`),但绝不能携带 `max_turns`、`write_paths`、重试或验证策略这类按次取值——它们由任务或调度决定。Skill frontmatter 可以继续变胖;`agent.ProfileFromSkill` 是唯一的收窄点,路由元数据(triggers、auto-use、cost、freshness)到此为止,因为它决定的是**何时**选中一个 worker,而不是它怎么思考。`internal/agent/profile_boundary_test.go` 会在任何一次拓宽时失败。
### 3.11 子智能体以 host 裁决过的结论收尾
写入型子智能体通过调用 `complete_subtask` 结束运行,提交 `status`、`summary`、它被要求满足的 `acceptance_criteria`(每条附上实际跑过的命令或改动的路径),以及尚未解决的 `unresolved`。纯散文仍然接受,但它不再是父智能体据以判断的接口。
提交的 status 是主张,不是判决。在父智能体看到之前,host 会用自己的 receipt 核对每一条引用:`verification` 必须指向 host 记录为执行过的命令,`diff`/`files` 必须指向 host 观测到读写过的路径,而 `manual` 永远不能自证。receipt 无法背书的条目一律降级为 `unsatisfied`,含有此类条目的报告不能保持 `complete`,且降级连同原因一并打印。host 只会下调,永不上调。
因此父智能体收到的顺序是:裁决后的 status 与条目、子智能体自己的散文、host 关于改了什么和跑了什么的 receipt。
### 3.12 写入声明是强制执行的,不是建议
`write_paths` 是调度与强制执行共用的同一个真相来源。写入型子智能体声明了显式路径后,host 会在子智能体启动前把它的工具注册表绑定到该声明:
- 支持路径参数的内建写工具(`write_file`、`edit_file`、`multi_edit`、`move_file`、`notebook_edit`、`delete_range`、`delete_symbol`)拒绝声明之外的任何路径,`move_file` 的源和目标两端都检查;
- 路径先解析到最深的存在祖先并展开 symlink 后再比较,因此 `..` 穿越和声明目录内指向外部的 symlink 都无法把写入洗白;
- 仅当 OS sandbox 能把 `bash` 的写根重绑到该声明时才保留 `bash`,否则直接从子智能体的注册表中移除;
- MCP 一律经 `use_capability`,它在解析阶段——任何 MCP 进程启动之前——拒绝所有未被证明为只读的目标;
- host 无法路径化约束的写工具(自定义、未知)被丢弃;
- 运行结束后,host 用自己记录的变更与声明比对,任何越界路径都会写进该子智能体的 host receipts 交还给父智能体。
省略 `write_paths` 并不等于不受约束:该次运行会声明整个 workspace,因而与其他所有写入声明串行。那是纯调度边界——workspace 内部不拒绝任何写入,因为同一时刻不可能有另一个持有重叠声明的并发写入者。但离开 workspace 的写入仍会被记为越界。
声明路径换来的是并行能力;代价是在 OS sandbox 无法强制写根的宿主上失去 `bash`。
### 3.13 子智能体的上下文继承是显式的
子智能体不隐式继承任何东西。它拿到的恰好是这些:
| 交给子智能体的 | 来源 |
| --- | --- |
| 系统提示 | `DefaultTaskSystemPrompt`、`DefaultReadOnlyTaskSystemPrompt` 或 profile body——不再合成任何其他内容 |
| workspace 根目录 | 首个 user turn 里的 `<workspace-context>` |
| 任务文本 | user turn 本身 |
| 完成契约 | 追加在写入型子智能体的任务 turn 后(见 §3.11) |
| 委派提示 | 嵌套子智能体全新会话上的 `<subagent-context>` |
| plan-mode 标记、推理/回复语言 | 运行选项(设置时) |
| 既有 transcript | 仅通过 `continue_from` / `fork_from` |
| 依赖的最终答案 | 首个 user turn 上的 `<upstream-results>`,仅限声明了 `depends_on` 的 fleet item(见 §3.14) |
按设计**不继承**:`REASONIX.md`、`AGENTS.md`、`CLAUDE.md`、项目与全局记忆(memory queue 被关闭,子智能体也无法写入记忆)、父对话、当前 Goal、planner 输出,以及它**没有声明依赖**的那些同级子智能体的结果。今天要让一条约束抵达子智能体,只能写进它的 profile body、任务文本,或一条 `depends_on` 边——不存在环境通道。
每次运行都会在其 transcript sidecar 中记录一份 `ContextCapsule`:workspace、系统提示来源与哈希、解析后的工具范围与 schema 哈希、model 与 effort、父会话与父工具调用 id、续接的 transcript,以及一个 `inherited` 块——其中各字段均为 false,唯一的例外是被 `depends_on` 边喂养过的条目上的 `upstream`。`capsuleHash` 是它的稳定标识,因此"为什么这个 reviewer 没看到那条约束"可以从记录回答,两次行为不同的运行也可以直接比对而不是猜。capsule 只保存引用与摘要——绝不复制父上下文,这正是委派保持低成本、子前缀保持可缓存的原因。
### 3.14 fleet 是一张小依赖图
fleet item 可以声明 `id` 与 `depends_on`。图的词汇就这么多:没有条件、没有表达式、没有动态扩散。它足以表达
```
research ──▶ implement backend ──┐
└──▶ implement frontend ─┴──▶ integration test ──▶ review
```
id 默认取 1 起的序号。重复 id、指向不存在任务的 id、自环、成环都会在 preflight 失败——一个注定跑不完的 fleet 绝不会开始。依赖完成后条目立即启动;彼此无序的条目仍按既有 session scheduler 并发。
依赖是图的性质,不是任务的性质:它只存在于 fleet plan 中,绝不进入 `TaskSpec`。这正是让 `depends_on` 不至于成为某种 workflow 语言第一个关键字的原因。
边携带的是依赖的答案,不只是它的次序。依赖项一开场就拿到每个已完成依赖的最终答案,装在 `<upstream-results>` 块里,按 `depends_on` 的声明顺序排列,并共享同一份字节预算——因此宽 fan-in 无法淹没开场 turn,也没有哪个依赖会被挤出预算之外。一条只排序、不交付任何东西的边,会让 `research → implement` 这一对为同一份 research 付两次钱。边交付的是上下文而不是指令,所以它走 `ContextRequest.Upstream`——绝不塞进任务文本(那必须保持交付分类所评判的那份原始目标,见 §3.11),也绝不挂在 profile 上(profile 描述的是一个 worker,不是一次运行)。
图恰好在该放松的地方放松了写声明 preflight:只有**可能同时运行**的条目才需要互不重叠的 `write_paths`;`implement → review` 这对被边串行化,可以共享路径——扁平 fleet 无法表达这一点。
失败处理只有一个开关。失败或被跳过的任务永远会跳过其整条下游分支——在坏输入上跑依赖项,只会换来父智能体必须丢弃的结果。除非设置 `fail_fast`,独立分支继续推进;`fail_fast` 停止的是**启动**新任务,已在运行的任务留待自然结束,因此写入者绝不会被中途丢弃。
失败携带的是身份,不只是一句话。失败的条目在状态旁报出 `reason=`——`provider.transient` 表示一次后续尝试有可能通过,`provider.rejected` 表示重试帮不上忙,`context.exhausted` 表示输入超出了窗口且压缩已无力回天——而被它杀掉的整条分支继承同一个 reason,因为一个被跳过的条目值不值得重发,恰好取决于它的起因值不值得重发。分类只读错误本身已经携带的身份(`*provider.APIError` 的状态码、`ErrCompactionRequired`、被中断的流),绝不读它的消息文本,所以一个生产者没有命名的错误会被如实报成 unclassified,而不是被猜出一个类别。于是「把这张图带着已完成的节点重发一遍」这个决定,读的是一个代码,而不是一句话。
一次跑到终点的运行不返回错误,无论其中失败了多少条目:失败由聚合报告,标题给出有多少个任务给出了答案。`context.Canceled` 只留给真正被打断的调用——那正是 turn loop 用来区分「被打断」与「跑完并且报告了」的东西。一条因上游失败而被跳过的分支,是那次失败的后果,不是一次打断;把它说成打断,代价由调用方承担。
一个节点可以让位给一条引用,而不是去运行。`adopt_ref` 指名一个已完成的子智能体,由它的答案顶替该节点:什么都不执行,所以该条目不持有任何 `write_paths`,不可能与活跃写入者相撞;它的依赖项照常通过普通依赖边拿到那份答案。它在 preflight 过的是与 `read_subagent_result` 完全相同的那道门——已完成、且位于本对话的血缘与 workspace 之内——因此一条无法被采纳的引用不会启动任何东西,而 `prompt` 与所有执行字段在它上面会被明确拒绝,而不是被静默忽略。
宿主不会做的那件事,是判断一个被采纳的答案是否仍然有效。结构里没有任何东西能说明 workspace 在它之下是否已经变了,而这个内核读不出结构的判断,它就不做——那一项留给列出这条引用的调用方,宿主只证明归属与完成。这也是为什么 plan 指纹不会静默复用匹配上的子智能体:调用方看不见的复用,就是它纠正不了的复用。配合 `list_subagents`,这就是一张被重启打断的 fleet 的恢复路径——枚举子智能体,把已完成的节点改写成采纳后重发同一张图,只为剩下的部分付钱。
“没有动态扩散”是被测量排除的,不只是一种口味。一个把某条目的 prompt 映射到另一条目所提交的若干 subject 上的 `for_each` 曾被完整实现,并在不进入 provider 可见 schema 的前提下上线,随后与它所替代的那次调用做了对照——同一张图,由一个任务处理全部 subject,那正是没有它时必须写成的样子。三个 fixture、二十八次针对真实 provider 的评分运行,两臂每一次都解出来了,而映射为此多花了 70% 到 98% 的 token(各自的精确置换检验 p ≤ 0.03)。第三个 fixture 是专门造出来让映射赢的:十个近乎相同的模块,答案是十次独立推导之和,任何一处闪失都是错答案。它在那里也没有赢。
原因在于:一个被映射出来的子智能体,每个 subject 都要付一次完整子智能体的固定成本——前缀、推理、收尾契约——而一个带着工具带的单个子智能体,根本不必去“持有”这些 subject。它一次读一个,当事实是可提取的,它就直接 grep。“一个 context 装不下 N 个 subject”这句话,预设了一次智能体其实从来不必执行的 ingestion。相关代码与其测试套件已被移除;如果哪天真出现需要按 subject 隔离的案例,历史里还留着它们。
### 3.15 只有一个子智能体构造原语
对外能派生子智能体的 API 很多——`task`、`read_only_task`、`fleet`、`parallel_tasks`、`run_skill`、`/<profile>`、`reasonix subagent run|try`、桌面端预览。它们背后的执行原语必须只有一个:每个入口把请求编译成 `ProfileExecSpec`,交给 `TaskTool.RunProfileSpec`——那是唯一解析深度、工具范围、权限、sandbox、写声明、调度槽位、MCP 前端、transcript 与 capsule、evidence ledger 以及完成契约的地方。
这不是风格偏好。散落在多条构造路径上的安全边界,只要被漏掉一次就够了:此前预览路径构造出未受约束的文件工具、profile 编辑器保存时丢掉 `read-only`,都是某一个入口少套了一层。
必须不持久化 transcript 的入口用 `ContextRequest.Ephemeral` 声明,而不是自己造一个 session——它的承诺是 spec 上的一个字段,而不是第二条构造路径。
`internal/agent/spawn_boundary_test.go` 登记了仍然直接调用底层 runner 的文件,出现新的就失败。剩余条目——`internal/boot`(skill runners)、`internal/cli/review.go`、`desktop/subagents_app.go`——是已知负债,不是先例。
### 3.16 MCP 并发:read-only 不等于 stateless
子智能体共享一个 session Host 及其连接,各自持有独立的 `use_capability` 前端与 ledger。对 stdio 服务器而言,这意味着它们共享同一个进程——以及那个进程的会话状态。
read-only 并不蕴含 stateless。浏览器类服务器会打开页面、切换标签、滚动;这些工具**完全可能诚实地声明 `readOnly`**(确实没有任何东西落到文件系统),但两个子智能体并发调用它,就会在彼此都看不见的状态上交错。写声明在这里帮不上忙——根本没有可声明的东西。
因此每个已配置服务器带一条并发策略:
```toml
[[mcp.servers]]
name = "browser"
concurrency = "serial" # parallel(默认)| serial
```
`serial` 表示整个 session 内该服务器同一时刻只跑一次调用,无论由哪个子智能体发起。闸门放在共享 runtime 上,因为被交错的那个进程正好就是这个作用域共享的;排队中的调用仍然响应自身的取消。名字看起来是已知有状态的服务器(browser、playwright、puppeteer、chrome、chromium、selenium)默认 `serial`;显式配置永远优先,其余一律保持 parallel,共享 Host 的性能取舍不变。
这是刻意保守的第一版:**一个服务器一条策略,而非按 capability**。按工具的 `parallel_safe` / `exclusive` 提示与显式 `concurrency_key` 分组是后续细化,等真实服务器暴露出同一服务器内工具确有差异时再做。
### 3.17 度量委派是否真的划算
编排容易加、难证明:agent 越多 token 一定越多,而多烧的 token 本身就可能看起来像"变好了"。因此对比实验臂必须**固定模型**并读取 host 记录的事实,而不是散文。
`reasonix run --json` 在既有的 token / cache / 成本 / 耗时之外,额外输出每次运行的委派计数:
| 计数 | 回答什么 |
| --- | --- |
| `subagent_runs`、`subagent_nested_runs` | 实际跑成了什么形状(而非配置成什么) |
| `tool_calls` − `subagent_tool_calls` | 父/子工作量切分 |
| `subagent_mutations`、`duplicate_work_paths` | 是否有两个子智能体重做了同一个文件 |
| `completion_reports`、`completions_prose_only` | 多少运行以可检验的主张收尾 |
| `false_completions`、`criterion_downgrades` | host 拒绝背书的主张 |
| `write_scope_violations` | 逃出声明的写入 |
控制轴目前是**不完整**的,而这正是这些计数暴露出来的:`--ablate subagent` 移除 `task`、`read_only_task`、`fleet`、`parallel_tasks`,但运行仍可通过 `runAs=subagent` 的 profile skill 委派——实测中 `no-subagent` 臂就把一次子运行花在了 `explore` 上。因此该臂应理解为"无 task 工具委派",而非"单 agent";实际发生了什么要读 `subagent_runs`,不要相信臂的标签。嵌套深度由 `agent.max_subagent_depth` 控制。
`false_completions` 是其中最关键的一个。它来自 §3.11 的裁决,因此度量的是 **host 拒绝背书**的主张,而不是某个评审者的观感——它是区分"fleet 更快完成了"与"fleet 声称完成了"的唯一数字。
读这些数字时必须对照实测的**噪声底**:同一个臂在同一批任务上重复跑一次,逐任务 token 用量的中位差为 19%、最大 54%,而该次实验里两臂之间的总差异只有 2.5%。因此每格只跑一次,对委派得不出任何结论——效应必须先高过方差才算效应。要么预算足够的重复次数,要么只在 `subagent_runs` 显示确实发生了委派的任务上比较——那次实验里六道题只有一道发生了委派。
目前实测到的结论(单一模型、四种任务形状,每种都用中立 prompt 与强制委派孪生题在**同一份工作**上对比):三个独立模块各一行修改 3.8x tokens;24 文件搜索 1.5x tokens / 2.2x wall;36 文件三包迁移 2.6x tokens / 4.1x wall;三个真正异质的分支——理论上委派最有胜算的形状——三次重复下 2.4x tokens / 3.7x wall。成功率四种形状全部 100%;强制臂的离散度约为中立臂的两倍——委派同时买来了方差。
子 agent 的 token 数字要小心解读:实测 27 个子运行平均每个 13.4 万 tokens,但那是 9.3 次模型调用上**同一份约 1.4 万上下文被反复重发**的累计值,不是 13.4 万条新内容。在约 90% 缓存命中下,一个子 agent 的真实价格平均为 **¥0.017**。真正有意义的是上面那个 2–4 倍——因为两臂用同一种口径计数;单个子 agent 的累计值不是一个可以拿来和"分支工作量"比大小的阈值。
委派为何罕见,可以从同一批运行里得到答案,而答案**不是"模型权衡后拒绝"**。在 33 次可委派的运行中只有 15% 发生了委派,bash 与全部委派类调用之比是 10:1。记录下来的推理显示,模型反复权衡的是**怎么高效地读**——"that's 25 files... read them in parallel batches... I can read multiple files at once"——而这道题正是为 `explore` 设计的,委派却从未进入它的决策空间。
三个原因可以解释,其中只有一个算缺陷。基础系统提示从未提及委派;所有提及都在技能索引里,且每一处都是刹车("the heavy path... only when the task genuinely needs context-heavy work, not on weak relevance"),紧挨着的却是对内联技能的油门("even plausibly relevant... cheap")。`task` 工具的描述只说它做什么,从不说何时该用它。而模型本就拥有更便宜的并行——**一次往返里发起多个工具调用,不复制任何上下文**——它正是按这个在推理。
考虑到实测的 2.4–4.5 倍代价,"刹车"是正确的默认值;真正的缺口是**没有任何机制能识别出委派确实划算的那少数情况**。强行委派并不能补上这个缺口:在强制 fleet 的那次运行里,父智能体在派发之前就已经在自己的推理中得出了全部三个修复,子智能体只是重新读了一遍代码去执行父智能体已经想好的编辑。**委派转移的是打字,不是思考。**
有一个假设是**未被证伪、而是没测成**:委派的隔离价值应当在父智能体真正被"读过的东西"拖累时才显现。这里没能制造出这种压力——把工作区 `compact_ratio` 压到 0.5% 仍然是零次压缩,因为 agent 靠写脚本而不是靠读把会话维持得很小,而这恰恰就是它赢下每一次对比的同一个行为。要制造上下文压力,需要一道无法被脚本化绕开的题,当前语料里还没有。
其中最有启发的是迁移那道题:不干预时 agent 只读了一个文件、写了个脚本,28 秒改完 108 处调用点;一旦按包切成三份,没有任何一个分支能看见那个一次解决全部三包的变换。**"看起来像并行形状"不构成"切开更便宜"的证据。**
尚未度量、且刻意不伪造的一项:handoff 后返工需要整次运行的变更时序,它属于驱动实验臂的 harness,而不属于记录单次运行的仪器。
## 4. 数据类型
provider 层的核心类型包括 `Role`、`Message`、`ToolCall`、`ToolSchema`、`Request` 和 streaming `Chunk`。`Message` 保留 `tool_calls`、`tool_call_id` 与 `name`;`Chunk` 区分 text、tool call、done 和 error。字段定义以英文规范及 `internal/provider` 源码为准。
## 5. 配置
配置优先级:
```text
flag > ./reasonix.toml > 用户 config.toml > 内置默认值
```
从 v1.8.1 起,用户配置位于 macOS/Linux 的 `~/.reasonix/config.toml` 或 Windows 的 `%AppData%\reasonix\config.toml`。provider key 保存在 Reasonix home 的 `.env`;项目 `.env` 只用于 workspace 范围的非 provider 变量展开。完整路径见[配置路径](./CONFIG_PATHS.zh-CN.md)。
```toml
default_model = "deepseek"
[agent]
temperature = 0.0
reasoning_language = "auto"
[[providers]]
name = "deepseek"
kind = "anthropic"
base_url = "https://api.deepseek.com/anthropic"
# request_url = "https://proxy.example.com/anthropic/v1/messages" # 可选:完整请求地址
models = ["deepseek-flash", "deepseek-v4-pro"]
default = "deepseek-flash"
api_key_env = "DEEPSEEK_API_KEY"
web_search = true
context_window = 1000000
# max_output_tokens = 0 # 推荐:自动(DeepSeek 默认 high → 约 64K)
# max_output_tokens = 32768 # 普通编码 / 控制费用
# max_output_tokens = 65536 # 重推理、长工具链
# max_output_tokens = 131072 # 仅在反复 finish_reason=length 时再考虑
[tools]
enabled = []
bash_timeout_seconds = 120
mcp_startup_timeout_seconds = 30
mcp_call_timeout_seconds = 300
[permissions]
mode = "ask"
deny = ["Bash(rm -rf*)", "Bash(git push*)"]
allow = ["Bash(go test:*)", "Bash(git status:*)"]
[sandbox]
# workspace_root = ""
# allow_write = ["/tmp"]
# forbid_read = ["${HOME}/.ssh"]
[serve]
auth_mode = "none"
```
原生 CLI 更新器始终安装最新的严格 `vX.Y.Z` 正式版。1.x 期间仍解析旧渠道配置与
参数,但统一指向正式版,并在后续保存配置时省略这些字段。
`[sandbox]` 是权限策略之下的强制执行层。file writer 默认限制在 workspace root、Reasonix 用户配置目录和 `allow_write`;`forbid_read` 可阻止读取敏感路径。macOS 使用 Seatbelt,Linux 使用 bubblewrap;若声明 enforce 但平台 backend 不可用,Bash 应拒绝执行而不是静默降级。Windows 当前没有 OS 级 Bash sandbox,file tool 的路径限制仍然生效。
`[serve]` 控制 `reasonix serve` 的 browser frontend。默认 `auth_mode = "none"` 仅适合 loopback;暴露到其他机器时必须使用 token 或 password。只有位于可信 reverse proxy 后方时才能启用 `behind_proxy`。
项目根目录的 `.mcp.json` 可使用 Claude Code 的 `mcpServers` schema;与 `reasonix.toml` 同名时,以后者为准。
MCP 启动与单次工具调用使用不同生命周期。调用方只短暂等待冷启动,而共享的进程启动、授权、
`initialize`、`tools/list` 可在后台继续,最长由 `mcp_startup_timeout_seconds`(默认 `30`)
限制;单个服务器可用 `startup_timeout_seconds` 覆盖。MCP 调用超时只在连接就绪后开始计算。
## 6. 错误处理
- library code 使用 `fmt.Errorf("...: %w", err)` 包装并返回错误,不打印也不调用 `os.Exit`;
- 只有 `cli` / `main` 决定 exit code 和面向用户的信息;
- tool error 返回给模型,不直接终止 agent loop;
- network layer 应对 429 / 5xx 使用有界指数退避。
## 7. 代码风格
- `gofmt`、`go vet` 必须通过;
- package name 使用小写,exported identifier 必须有文档;
- 注释解释“为什么”,而不只是复述“做了什么”;
- 避免过早抽象,优先清晰直接的实现。
## 8. 分发
- 构建:`CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=$(VERSION)" -o reasonix ./cmd/reasonix`
- 目标矩阵:`darwin|linux|windows × amd64|arm64`
- 版本通过 ldflags 注入,来源为 `git describe --tags --always`
- 支持预编译二进制、`go install` 与 Homebrew。
## 9. 路线图(当前范围之外)
- 完成 Sandbox Phase 1 的 escape prompt:检测 sandbox 不可用或拒绝时,提供一次明确、受权限控制的非 sandbox 重试。
- MCP long tail:`headersHelper`、更多 `.mcp.json` scope、tool-search 延迟加载、`list_changed`、channel、elicitation、root,以及可提供 provider 的插件。
- 增加 Anthropic-native provider kind,用于验证 registry 不依赖单一 wire format,并支持原生 prompt cache control。
- 把“始终允许”规则持久化到项目配置,以及为 `reasonix run` 提供 session 级权限覆盖。
-176
View File
@@ -1,176 +0,0 @@
# 子智能体 Profile
子智能体 Profile 是可复用、显式调用的专用智能体,适合代码评审、问题调查、文档整理等
聚焦任务。每个 Profile 都是带有 `runAs: subagent` 的手动 Skill:Reasonix 会启动隔离的
子智能体,把 Profile 提示词和任务交给它执行,并且只把最终答案返回父智能体。
桌面端、交互式 CLI 和 Headless CLI 共用这些 Profile。它们直接复用现有 Skill 文件格式和
目录,不引入独立数据库。
## 创建 Profile
从提示词文件创建项目级 Profile:
```bash
reasonix subagent create reviewer \
--description "检查改动的正确性和回归风险" \
--prompt-file reviewer.md \
--tools read_file,grep,bash \
--model deepseek-pro \
--effort high
```
在 workspace 中,`create` 默认使用 project scope;不在 workspace 中时默认使用 global
scope。也可以通过 `--scope project` 或 `--scope global` 明确指定。项目级 Profile 存放在
`.reasonix/skills/<name>/SKILL.md`,全局 Profile 存放在 Reasonix home 的 Skill 目录中,
具体路径见[配置路径](./CONFIG_PATHS.zh-CN.md)。
提示词可来自 `--prompt`、`--prompt-file PATH`、`--prompt-file -` 或标准输入:
```bash
printf '%s\n' '检查任务,只报告可执行的问题。' | \
reasonix subagent create reviewer --description "代码评审"
```
名称可以包含字母、数字、`_`、`-` 和 `.`。如果名称已经被项目级、全局、自定义或内置 Skill
占用,Reasonix 会拒绝创建,避免覆盖已有内容。
## 调用 Profile
在交互式 CLI 或桌面聊天中使用斜杠命令:
```text
/reviewer 评审当前 diff
```
这会真正启动隔离子智能体,并非把提示词文本注入父智能体。父会话只保留任务和子智能体的
最终答案,不保留子智能体的完整工作上下文。
父模型也可以在调用时选择 Profile,且不会把 Profile 名称列表写进工具 schema(保持
prompt-cache 稳定):
```text
task(profile="doc-rewriter", prompt="重写 docs/01.md", write_paths=["docs/01.md"])
fleet(tasks=[
{profile="doc-rewriter", prompt="重写 docs/01.md", write_paths=["docs/01.md"]},
{profile="doc-rewriter", prompt="重写 docs/02.md", write_paths=["docs/02.md"]}
])
```
- `task` / `fleet` 项上的 `profile` 按名称解析 `runAs: subagent` Skill(显式名称可调用
`invocation: manual` Profile)。
- Profile 正文成为子智能体的**完整**系统提示词,不再隐式叠加 concise 默认提示。
- `write_paths` 声明互不重叠的写入目标,使多个写入子智能体可共享同一工作区并行。
写入任务若省略 `write_paths`,则声明整个工作区(与其他写入 claim 互斥)。
在 `fleet` 中,多个整工作区 claim 或任何路径重叠会在预检阶段整批失败,不会启动任何任务。
- 会话默认:`agent.max_subagent_concurrency = 6`、`agent.max_parallel_writers = 3`
(均可配置为 1–32,且写入上限不得超过总上限)。
脚本和其他 Headless 场景应使用显式命令:
```bash
# 使用只读工具预览。
reasonix subagent try reviewer "评审当前 diff"
# 按正常权限和沙盒策略运行。
reasonix subagent run reviewer "评审并修复当前 diff"
# 从标准输入读取任务,并限制工具调用轮次。
git diff | reasonix subagent run reviewer --max-steps 20
```
`run`/`try` 的参数应放在任务文本之前。两个命令都支持 `--model REF` 和 `--dir PATH`。
`try` 始终选择只读 runner;`run` 使用正常的隔离 runner,权限中的 `deny` 规则和沙盒限制
仍然有效。普通 `reasonix run` 仍是单次任务入口,不会隐式解释 `/<profile>` 语法。
## 管理 Profile
```text
reasonix subagent list [--dir PATH]
reasonix subagent create <name> --description TEXT (--prompt TEXT | --prompt-file PATH)
[--scope project|global] [--model REF] [--effort LEVEL]
[--tools a,b] [--color NAME] [--dir PATH]
reasonix subagent edit <name> [--description TEXT]
[--prompt TEXT | --prompt-file PATH] [--model REF] [--effort LEVEL]
[--tools a,b] [--color NAME] [--dir PATH]
reasonix subagent delete <name> --yes [--dir PATH]
reasonix subagent try <name> [--model REF] [--max-steps N] [--dir PATH] <task>
reasonix subagent run <name> [--model REF] [--max-steps N] [--dir PATH] <task>
```
`edit` 只修改命令行中显式提供的字段。用显式空值清除可选字段:
```bash
reasonix subagent edit reviewer --model= --effort= --tools= --color=
```
省略工具列表或将其清空,表示 Profile 不额外添加工具白名单;runner 原有的工具可用性、权限、
沙盒和只读规则仍然有效。`delete` 必须带 `--yes`,不会发生隐式删除。
内置 Profile 没有可写的 Skill 文件。对它们执行 `edit` 时只支持 `--model` 和 `--effort`,
保存的位置与桌面设置页使用的按 Profile 覆盖配置相同;传入空值会删除对应覆盖。
## 文件格式与高级 Profile
CLI 和桌面 Profile 编辑器会生成精简的 Skill 文件:
```yaml
---
name: reviewer
description: 检查改动的正确性和回归风险
color: orange
invocation: manual
runAs: subagent
model: deepseek-pro
effort: high
read-only: true
allowed-tools: [read_file, grep, bash]
---
你是专注的代码评审员。检查指定改动,只返回可执行的问题,并按严重程度排序。
```
`invocation: manual` 表示模型不会从固定 Skill 索引中自动发现该 Profile,但用户仍可显式
调用。`allowed-tools` 是 Profile 级工具白名单,不能绕过权限系统。`read-only: true`
强制使用只读工具 registry(剥离写入工具);省略/`false` 保持旧版默认可写。
也可以手写更丰富的 `runAs: subagent` Skill,例如使用自定义 Skill path 或额外 frontmatter。
这些 Profile 可以被列出和调用,但 Profile 编辑器会拒绝编辑或删除以下内容:
- 不属于 project/global scope 的 Profile;
- `invocation` 不是 `manual` 的 Profile;
- 含有编辑器无法管理的 frontmatter 的文件;
- 含有 `references/` 或 `scripts/` 目录的 Skill。
这样可以防止精简编辑器静默丢弃高级 Skill 内容。此类 Profile 应直接按 Skill 文件管理。
## 模型与推理强度选择
有效模型和推理强度按以下优先级选择:
1. `agent.subagent_models` 和 `agent.subagent_efforts` 中按 Profile 设置的覆盖;
2. 本次 `task` / `fleet` 调用参数中的 `model` / `effort`;
3. Profile frontmatter 中的 `model` 和 `effort`;
4. `agent.subagent_model` 和 `agent.subagent_effort` 默认值;
5. 已配置的 executor/默认模型及其默认推理强度。
例如:
```toml
[agent]
subagent_model = "deepseek-pro"
subagent_effort = "high"
subagent_models = { reviewer = "deepseek/deepseek-v4-pro" }
subagent_efforts = { reviewer = "max" }
```
`subagent run` 或 `subagent try` 的 `--model` 参数用于选择该 Headless 命令初始化时的默认
模型;Profile 专属配置仍按上述优先级生效。
## 桌面端同步与排障
桌面设置页和 `reasonix subagent create` 创建的 Profile 共用同一批文件。修改 Profile 后,
请刷新或新建会话,让已经运行的会话重新加载 Skill registry。
如果调用时报 Profile 未知或已禁用,请检查 `reasonix subagent list`、当前 `--dir` 和
`skills.disabled_skills`。如果编辑时报 custom 或 rich Profile,应直接编辑其 `SKILL.md`,
不要强行经过 Profile 编辑器。Reasonix 在解析有效模型时会拒绝未知模型引用和无效 effort。
-73
View File
@@ -1,73 +0,0 @@
# 本地子 Agent 进度展示
状态:**已实现** —— 桌面端与 CLI 为本地子 Agent 运行(`task`、`read_only_task`、`parallel_tasks`、`fleet`)提供逐子任务的进度预览,构建在已持久化的子 transcript 与 `read_subagent_result` 之上(持久化模型见 [`CHECKPOINTS.md`](CHECKPOINTS.zh-CN.md))。
## 目标
子 Agent 工作时,用户应能看到**它正在做什么**,且子 Agent 的 reasoning/正文不进入父对话:进度卡片显示子任务的阶段、运行耗时与最近活动;桌面卡片可展开查看受限的 reasoning / 回答 / notice 预览;CLI 在 `/verbose` 模式下显示同样的预览。全部零配置——不新增任何设置项。
## 线上合同
进度预览复用现有 `ToolProgress` 事件,使用四个保留的 `Tool.Name` 值。这些名称是 agent 进度 tracker 与本地前端之间的内部合同;绝不能作为 provider 可见的工具名出现:
| 名称 | 载荷 |
|---|---|
| `reasonix.subagent.status` | 恰好为 `queued`、`running`、`reasoning`、`responding`、`tool`、`retrying`、`completed`、`failed`、`cancelled` 之一 |
| `reasonix.subagent.reasoning` | 受限的 UTF-8 文本增量(子任务的思考) |
| `reasonix.subagent.text` | 受限的 UTF-8 文本增量(子任务的回答预览) |
| `reasonix.subagent.notice` | 受限的 UTF-8 文本增量(子任务的提示) |
字段约定:
- `Tool.ID` —— 子任务卡片 ID(进度查找以 ID 为准,绝不依赖正文)。
- `Tool.Output` —— 阶段值(status)或文本增量(预览)。
- `Tool.Truncated` —— 本轮预览发生截断或合并时为 `true`。
- `Tool.DurationMs` —— 最终耗时,随 terminal 状态事件携带。
- `Tool.ParentID` —— 沿用现有嵌套关系(顶层 `task` 为空;`parallel_tasks`/`fleet` 子任务为组调用 ID)。
## 行为
状态机(由统一执行链 `RunProfileSpec` 发出,`task`、`read_only_task`、`parallel_tasks`、`fleet` 共用,不在各入口复制):
- 前台运行以 `running` 开始。
- 后台任务在注册成功后发出 `queued`,真正获得执行槽时发出 `running`。
- `parallel_tasks`/`fleet` 组卡片拥有自己的显式生命周期:children 开始时分发 `running`,所有 children 落定后发出唯一 terminal(`completed`;取消/deadline 为 `cancelled`;任一 child 失败或调用出错——包括验证失败——为 `failed`)。前端绝不根据"当前已观察到的 children"推断组完成,因为后台 children 是异步分发的,快的首个子任务可能在后续子任务出现前就已完成。
- 子任务的 `Reasoning`/`Text`/`Notice`/`Retrying` 事件转换为对应预览频道;子任务真实工具活动把阶段更新为 `tool`,嵌套工具卡片渲染不变。
- 每次运行恰好发出**一个** terminal 状态:成功为 `completed`,context 取消或 deadline 为 `cancelled`,provider/工具/存储/panic 错误为 `failed`。terminal 前同步 flush 待发送预览;terminal 后的迟到事件被忽略。
限流与内存边界(按父任务组):
- 每个 (子任务, 频道) 只保留一个待发送槽;预览最多合并 250ms 后发出一条事件,增量不会无界累积。
- 每组每秒最多 32 条非终态事件——阶段变化与内容预览共享同一预算,按子任务轮转,避免高活跃子任务饿死其他任务。仅初始 `queued`/`running` 状态与 terminal 事件不受限。
- 预算裁剪丢弃缓冲内容时,丢失会以 `Truncated` 标记传播到下一条实际发出的频道(或在 terminal flush 时以截断 notice 呈现),前端总能得知部分预览被丢弃。
- 每个子任务未发送缓冲总计上限 8 KiB(优先丢弃 notice,其次 reasoning,最后 text);超出后保留 UTF-8 安全的尾部并设置 `Truncated`。桌面端按频道保留(reasoning/text 各 8 KiB、notice 2 KiB);CLI 为 `/verbose` 保留 4 KiB reasoning/text 尾部。
明确不做:
- 子任务的 `Message`、reasoning 与正文绝不进入父 transcript 或 provider 上下文。
- 不新增事件 kind、不新增线上字段、不改 provider 工具列表/工具 schema/system prompt、不新增配置。
- 预览不持久化:重启后完整子 transcript(与 `read_subagent_result`)仍是事实来源。
- ACP 与 bot 消费者继续整体忽略 `ToolProgress` 正文。
## 桌面端
- 子 Agent 工具卡片的头部显示阶段徽标(阶段 + 运行耗时 + “N 秒前”最近活动);子任务存活期间每秒跳动一次,结束后定格为阶段 + 时长摘要。
- 展开卡片显示独立的 reasoning / 回答预览 / notice——绝不与普通工具输出混排。
- 后台调用即使已返回 job id,只要子进度仍为非终态,卡片仍保持运行状态;`parallel_tasks`/`fleet` 组卡片只由其自身生命周期 terminal 事件定格——job-id result 先于任何子任务到达、或快的首个子任务先于后续子任务完成,都不会让组卡片提前定格。
- `completed`/`failed`/`cancelled` 分别沿用现有 done/error/stopped 视觉语义;terminal 后默认折叠,用户手动展开的选择在状态变化后保留。
## CLI
- 每个子任务维护独立进度状态与固定 transcript 槽位(按调用 ID 键控),独立于单一 live 工具流——并发子任务绝不串流。
- 默认只显示阶段、耗时与最近活动;reasoning/正文在 `/verbose`(Ctrl+O)模式下显示,受限为最近 4 KiB 尾部。
- terminal 后默认折叠为一行摘要;verbose 保留受限预览。
- 无法原地重绘的终端(Termux native scrollback)仅在阶段变化与 terminal 时输出状态行;verbose 预览每子任务每 2 秒最多输出一次。
## serve
- 携带 `parentId` 的调用渲染在父卡片内部,绝不作为顶层条目:被委派的命令不能被读成会话自身的操作。父卡片运行期间展开,定格时折叠——用户手动切换过则保留其选择。
- 窄屏布局隐藏状态徽标,但被主机拒绝的调用(shell 状态 `not_run`)例外——此时仅凭红色图标会被读成“失败”而非“从未执行”。
## 合同稳定性
前端按 `reasonix.subagent.` 前缀匹配保留名称,因此较新 agent 新增的频道会被较旧前端忽略(绝不追加进普通工具输出)。
-69
View File
@@ -1,69 +0,0 @@
# 任务合约与暂停策略
<a href="./GUIDE.zh-CN.md">使用指南</a>
&nbsp;·&nbsp;
<a href="./TASK_CONTRACT.md">English</a>
复杂任务最适合写成一份**任务合约**:背景是什么、要完成什么动作、结果怎么交付、哪些边界不能越过,以及什么时候才需要暂停问用户。
有些 prompt 模板把最后一段叫 “Checkpoint”;Reasonix 文档里叫 “Pause policy”,避免和 Checkpoints/Rewind 快照混淆。
这不是更长的角色设定。更强的 coding agent 通常不需要用户教它一步步思考;它更需要清楚的任务边界和验收标准。
## 模板
```text
Context:
我正在做 [大任务]。
目标对象是 [谁]。
这个结果要帮助他们 [达成什么结果]。
Request:
请完成 [一个明确动作]。
Output format:
请按照 [具体结构] 输出。
必须包含 [必要模块]。
不要超过 [长度/范围]。
Constraints:
不要 [错误假设]。
不要 [越界内容]。
不要 [低质量输出形式]。
如果 [信息不足],请明确标注不确定性。
Pause policy:
除非下一步涉及不可逆或对外可见操作、任务范围变化,或需要我提供信息,否则请继续完成任务后再汇报。
```
## Reasonix 如何使用它
- **普通聊天**可以直接粘贴这份模板,适合一次性任务。
- **Goal 模式**会把目标当作任务合约持续推进,直到 request、output format、constraints 和必要验证都满足。
- **计划模式**适合“先产出并确认方案,再进入实施”的场景;它是工作流指令,不是只读权限边界。
- **工具审批**仍然独立生效:写文件、跑 shell、发布、凭证、外部副作用都会继续遵守配置的审批策略。
- **Checkpoints/Rewind** 是代码和会话快照;这里的暂停策略只描述 agent 什么时候应该停下来问用户。
Goal 模式里的任务合约会随 user turn 注入 provider 可见上下文,不会改写 cache-stable system prompt、memory prefix 或 tool schema。
## 示例
```text
/goal Context:
我正在改进桌面端输入框。
目标对象是需要连续做代码审查的用户。
这个结果要帮助他们避免被补全菜单打断输入。
Request:
修复 slash-command 菜单打开时键盘焦点丢失的问题。
Output format:
完成后按“改了什么 / 验证结果 / 剩余风险”汇报。
Constraints:
不要改变 Wails JSON 合约。
不要顺手重构无关的 composer 状态。
如果无法跑浏览器验证,请说明原因。
Pause policy:
除非下一步需要产品判断、公开 push 或凭证,否则继续完成实现和验证后再汇报。
```
-27
View File
@@ -1,27 +0,0 @@
# 官方主题素材来源与许可记录
八款 Reasonix 官方主题的全部图片均为**原创**,由 `scripts/official-theme-art/` 中的脚本
从零程序化生成(numpy + Pillow,固定随机种子,可完全复现)。视觉*方向*参考了 MIT 许可的
[Codex-Dream-Skin](https://github.com/Fei-Away/Codex-Dream-Skin) 概念图库,但是:
- **未复制参考项目或任何第三方素材的任何像素**、布局、界面元素、文字、标志或水印;
所有背景均为代码重新生成的独立素材。
- 图中人物均为生成器绘制的**原创虚构成年人**:插画女性(玫瑰晨光)、吉祥程序员(鸿运工坊)、
读者(鼠尾草清风)、动漫人物(灵感手账)、剪影女性(紫曜星夜)、数字表演者(青岚舞台)、
绅士(黑金序曲)。赤曜新城不含人物。
- 背景中不含窗口、侧栏、卡片、按钮、输入框或可读文字,并已去除 EXIF 等元数据。
素材随 Reasonix 仓库以 MIT 许可发布,© Reasonix Contributors。人工审核:Reasonix Contributors(发布 PR 审核)。
生成日期:2026-07-17
| 主题 | 生成器(最终提示词等价物) | background.webp SHA-256 | preview.webp SHA-256 |
| --- | --- | --- | --- |
| Crimson Horizon / 赤曜新城(`official-crimson-horizon`) | `scripts/official-theme-art/scenes.py::scene_crimson_horizon (seed 20260119, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `34e9051e6b47c64a3d3324832325f06811aaf36ca3a7f3e55480cb978343f1f8` | `ea26252f557aefafdad5147e509e73c4b2c284c50d4d38bee33edcf64499c0dd` |
| Cyan Stage / 青岚舞台(`official-cyan-stage`) | `scripts/official-theme-art/scenes.py::scene_cyan_stage (seed 20260123, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `e03e4474e9f0cef3e9f1f48c821962e1adf65cde16ac1659ea7cfe78f524cd5e` | `1fafc6e116f57f68ad8ff040ea6979f0d31769860090779b7a4bd6e04ff2b8ca` |
| Fortune Forge / 鸿运工坊(`official-fortune-forge`) | `scripts/official-theme-art/scenes.py::scene_fortune_forge (seed 20260118, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `f8b6f1d2eaea4a2d74990c0f4fe2958016f273a579e0637ceea794789b35780e` | `04c26ed966ff1fda5acd30a802a51484c140fc9c568acdacf98e5b59310a1255` |
| Noir Gold / 黑金序曲(`official-noir-gold`) | `scripts/official-theme-art/scenes.py::scene_noir_gold (seed 20260124, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `05684900a437a462a6fdc267ff8bc28abe33e311dd5d3d7e99e1e456a3ccf914` | `10df2fe7a3d0f4c6681be4b9421137a9a83894423ae5508c056dd4f996283197` |
| Rose Dawn / 玫瑰晨光(`official-rose-dawn`) | `scripts/official-theme-art/scenes.py::scene_rose_dawn (seed 20260117, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `80e92ffa4c94e7f976f3678116bf59beb3db6f2d322b7b5e5dfbc1d447cc99bc` | `0b7e2096ea67ee2a37d0b50e04ac459b78200aca35906e5051b12382fba37f1e` |
| Sage Breeze / 鼠尾草清风(`official-sage-breeze`) | `scripts/official-theme-art/scenes.py::scene_sage_breeze (seed 20260120, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `a27c4bf63edcb5b5aed2790b2f24d3d72aceedd21c78943c314ad1b11fc05344` | `55fed2b26a7f16b4ec0e144e2f89cf07d17ce4bd53b558c84a69b712044f8aa6` |
| Spark Notebook / 灵感手账(`official-spark-notebook`) | `scripts/official-theme-art/scenes.py::scene_spark_notebook (seed 20260121, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `adc70d90e9fdf6670a2fa6b7e46a878694ed9097995bae2a6d3e59bd21810092` | `f114dc160f3be0d8ff943b0dd9bbbcb93e6fc8c306fdbd5094daecc3f7e32a26` |
| Violet Starlight / 紫曜星夜(`official-violet-starlight`) | `scripts/official-theme-art/scenes.py::scene_violet_starlight (seed 20260122, 2560×1440 canvas, low-info left 0–52%, key content x 62–88% / y 16–72%)` | `22999e9796f77ad10bd43e21503bf07efa1274eddd4cc0449485fde3e8a830bc` | `9dfbe62b922d140dc89b4991154148f3986f00ab463df9ebb05a997e93c4cf3f` |
-192
View File
@@ -1,192 +0,0 @@
# Reasonix 主题包 V2
Reasonix 桌面端原生主题包。主题是**受控皮肤**:语义颜色令牌、密度/圆角配方,以及首页与任务/工作区可独立配置的本地背景图。主题**不能**执行 CSS、JavaScript、加载字体、远程 URL 或 SVG 脚本。V1 主题继续兼容,并在两个场景共用首页图片。
> English: [THEME_PACK.md](./THEME_PACK.md)
## 首版目标
- 内置风格、自建主题、背景图、实时预览、导入/导出、本地主题库
- 首页完整展示背景;进入任务后自动降低透明度并增加方向性遮罩
- 同时支持 Classic / Workbench / Creation,以及 `auto` / `light` / `dark`
- **不包含**在线主题市场、云同步或脚本插件
## 主题体验(设置信息架构)
外观拆成两个界面(不增加第三套入口):
1. **外观概览页** — 当前主题摘要、明暗模式、**唯一**基础配色控件、字体与缩放。主操作:**浏览主题**。
2. **主题画廊** — 官方 / 我的主题 / 基础配色三个分类;点击卡片仅选中;详情区隔离预览;
「临时预览」才覆盖全应用;只有「应用主题」会持久化。沉浸式预览是画廊详情的一部分。
状态模型(`desktop-theme-state.json` schema v2):
| 状态 | 含义 | 持久化 |
| --- | --- | --- |
| `themeMode` | 自动 / 浅色 / 深色 | 桌面配置 |
| `baseStyle` | Graphite…Amber | 桌面配置(`theme_style`) |
| `activeThemeId` | 仅官方、用户或插件主题 | `desktop-theme-state.json` |
| `selectedThemeId` / `previewThemeId` | 画廊选中 / 临时预览 | 仅前端内存 |
- `activeThemeId` **禁止**保存基础配色 id。选择基础配色会清空主题包。
- 应用主题包时保留 `baseStyle` 作为停用后的回退值。
- 明暗模式与主题包独立。
## 主题类型
画廊分四组:
| 类型 | 来源 | 可编辑 | 可删除 | 可导出 |
| --- | --- | --- | --- | --- |
| **基础风格** | 六种视觉方向(Graphite、Aurora、Slate、Carbon、Nocturne、Amber),无令牌覆盖 | 否(需先复制) | 否 | 否 |
| **官方主题** | 安装包内嵌的八款只读主题(清单 + 原创背景 + 缩略图,MIT) | 否(需先复制) | 否 | 否 |
| **我的主题** | 编辑器新建、复制,或导入 `.reasonix-theme` | 是 | 是 | 是 |
| **插件主题** | 已启用插件贡献的 `.reasonix-theme`(Manifest v2 `contributes.themes`),直接从插件目录读取——从不拷入用户库 | 否 | 否(禁用或卸载插件) | 否 |
- 14 个内置 id(6 基础 + 8 官方)均为**保留名**:保存、导入、覆盖复制与删除都会拒绝冲突。
- 激活官方主题时,仅把其 id 写入 `desktop-theme-state.json`;资源在运行时从嵌入副本读取。
- 对基础/官方主题执行「复制」会生成可编辑的用户主题(官方背景会拷入用户库),之后可编辑或导出。
- v1 若把基础配色 id 存为 `activeThemeId`,加载时会迁移到 `desktop.theme_style` 并清空活动主题。
- 插件主题的外部 id 形如 `plugin:<plugin>:<theme>`;包内 `id` 仍遵循既有 id 规则。无效的 contributed 文件会被跳过并在主题视图中给出警告,绝不致命。当活动 id 对应的插件缺失、被禁用或被卸载时,界面回退到已配置的基础风格,但该 id **保留**在 `desktop-theme-state.json` 中——重新安装同一插件即可恢复主题。保存/删除/复制/导出都会以只读为由拒绝插件主题 id。
### 八款官方主题
| ID | 名称 | 基础风格 | 画面 |
| --- | --- | --- | --- |
| `official-rose-dawn` | Rose Dawn / 玫瑰晨光 | graphite | 象牙白晨光、柔粉玫瑰、原创插画女性 |
| `official-fortune-forge` | Fortune Forge / 鸿运工坊 | amber | 朱红/金/玉绿工坊、原创吉祥程序员 |
| `official-crimson-horizon` | Crimson Horizon / 赤曜新城 | graphite | 珊瑚红未来城市天际线,无人物 |
| `official-sage-breeze` | Sage Breeze / 鼠尾草清风 | slate | 奶油纸张、鼠尾草、原创读者 |
| `official-spark-notebook` | Spark Notebook / 灵感手账 | aurora | 手账网格与文具、原创动漫成年人 |
| `official-violet-starlight` | Violet Starlight / 紫曜星夜 | nocturne | 蓝紫星空、蝴蝶、剪影女性 |
| `official-cyan-stage` | Cyan Stage / 青岚舞台 | carbon | 青蓝舞台与光环、原创数字表演者 |
| `official-noir-gold` | Noir Gold / 黑金序曲 | carbon | 黑丝绒、金色聚光灯、原创绅士 |
预览在应用内主题库(设置 → 外观)中展示,来自真实 Reasonix 构建。**请勿把应用截图当作主题背景导入。** 素材来源、哈希与许可记录见 [THEME_ASSETS.zh-CN.md](./THEME_ASSETS.zh-CN.md);生成脚本在 `scripts/official-theme-art/`(程序化、固定种子、可复现)。
## 包格式
以 `.reasonix-theme` ZIP 分发。根目录**只能**包含:
| 文件 | 必需 | 说明 |
| --- | --- | --- |
| `theme.json` | 是 | 清单(≤ 1 MiB) |
| `background.png` / `.jpg` / `.jpeg` / `.webp` | 否 | 首页图片 ≤ 16 MiB,边长 ≤ 8192 |
| `background-task.png` / `.jpg` / `.jpeg` / `.webp` | 否 | 独立任务/工作区图片 ≤ 16 MiB,边长 ≤ 8192(V2) |
ZIP 限制:包体 ≤ 36 MiB;禁止子目录、符号链接、重复条目与路径穿越。
### `theme.json` 示例
```json
{
"schemaVersion": 2,
"id": "my-theme",
"name": "My Theme",
"author": "",
"description": "",
"license": "",
"baseStyle": "graphite",
"tokens": {
"light": {
"bg": "#f4f3ef",
"fg": "#111827",
"accent": "#2f5fa8"
},
"dark": {
"bg": "#0c0d10",
"fg": "#f1f1ef",
"accent": "#ff6a3d"
}
},
"recipes": {
"density": "comfortable",
"corners": "soft"
},
"background": {
"image": "background.webp",
"focusX": 0.72,
"focusY": 0.45,
"safeArea": "left",
"homeOpacity": 1,
"taskOpacity": 0.28,
"overlayStrength": 0.62
},
"taskBackground": {
"image": "background-task.webp",
"focusX": 0.5,
"focusY": 0.5,
"safeArea": "right",
"opacity": 0.28,
"overlayStrength": 0.62
}
}
```
JSON Schema: [theme-pack.schema.json](./theme-pack.schema.json)
### 字段规则
| 字段 | 规则 |
| --- | --- |
| `schemaVersion` | `1` 或 `2`;使用 `taskBackground` 时必须为 `2` |
| `id` | 小写 `[a-z][a-z0-9-]*`;保留:`graphite`/`aurora`/`slate`/`carbon`/`nocturne`/`amber` |
| `baseStyle` | 六套内置方向之一;未覆盖令牌继承该方向 |
| `tokens.light` / `tokens.dark` | 可选;键 → `#RRGGBB` 或 `#RRGGBBAA` |
| `recipes.density` | `compact` \| `comfortable` |
| `recipes.corners` | `square` \| `soft` \| `round` |
| `background.image` | 仅允许裸文件名(png/jpeg/webp) |
| `background.focusX/Y` | 0–1 焦点 |
| `background.safeArea` | `left` \| `right` \| `center`(任务页遮罩方向) |
| `background.homeOpacity` | 0–1 |
| `background.taskOpacity` | 0–1 |
| `background.overlayStrength` | 0–1 |
| `background.paneOpacity` | 0–1(首页场景面板不透明度) |
| `taskBackground.image` | 可选的独立任务/工作区图片,仅允许本地裸文件名 |
| `taskBackground.focusX/Y` | 0–1 焦点 |
| `taskBackground.safeArea` | `left` \| `right` \| `center` |
| `taskBackground.opacity` | 0–1 |
| `taskBackground.overlayStrength` | 0–1 |
| `taskBackground.paneOpacity` | 0–1(任务场景面板不透明度) |
### 允许的令牌键
`bg`, `bgSoft`, `bgElev`, `panel`, `sidebar`, `chat`, `workspace`, `workspaceFiles`,
`border`, `borderSoft`, `fg`, `fgDim`, `fgFaint`, `accent`, `accentFg`, `ok`, `warn`, `err`
颜色**不得**包含 `url()`、渐变或任意 CSS。
## 引擎行为
1. 先应用全局 `auto`/`light`/`dark` 与基础视觉风格。
2. 再应用主题包覆盖层(CSS 变量),挂在样式表之后,避免被后置 `:root` 与 Creation 局部变量压掉。
3. 根节点 `data-theme-pack="<id>"`;应用容器 `data-theme-scene="home|task"`。
4. 场景仅由当前会话是否有内容决定,不改变聊天状态或布局生命周期。
5. 背景为独立、不可交互层。任务页限制最高透明度并叠加方向性遮罩(**不**使用 `backdrop-filter`)。
## 存储
| Reasonix 主目录路径 | 用途 |
| --- | --- |
| `desktop-theme-state.json` | 版本化的当前主题指针(**不**改 `config.toml`) |
| `themes/<id>/` | 用户主题库(`theme.json` + 最多两张可选场景图片) |
旧配置缺少主题状态时保持原行为;旧版本忽略新目录。CLI 主题、提示词、Provider 请求与缓存键均不变。
## 桌面桥接
列出 / 启用 / 重置 / 保存 / 删除 / 复制 / 导入 / 导出 / 选择背景。
前端只接收临时资源 URL(`/__reasonix_theme_asset/...`)或 data URL,不暴露本机绝对路径。
同 ID 导入默认拒绝,确认后才允许原子替换。内置主题不可覆盖或删除。损坏/丢失回退 Graphite 路径。安全模式不加载外部主题。`/theme reset` 与命令面板可恢复默认。
## 创作建议
1. 从内置方向起步,只覆盖需要的令牌。
2. 尽量满足 WCAG AA(正文约 4.5:1);编辑器会警告但允许继续保存。
3. 分享含背景图的主题前,确认照片/肖像/第三方素材的分发权利。
4. 首版不复制参考仓库的人物或第三方图片资产。
## 模板
无版权素材的纯色模板(不含背景图)见英文版 [THEME_PACK.md](./THEME_PACK.md) 中的 `paper-dawn` 示例;将仅含 `theme.json` 的根目录打成 `paper-dawn.reasonix-theme` 即可导入。
-150
View File
@@ -1,150 +0,0 @@
# 工具权限:询问、自动与 Yolo 模式
Reasonix 桌面端输入框下方的“询问 / 自动 / Yolo”控制的是工具权限审批方式。三种模式始终显示,可以直接切换,不必依赖快捷键或设置页。
工具权限和“协作方式”是两条独立轴:
- **协作方式与运行模式**决定 Reasonix 怎么推进任务,以及采用轻量、均衡还是交付优先倾向。
- **工具权限**决定 Reasonix 调用受控工具时,是否需要你审批。
## 快速对比
| 模式 | 行为 | 适合场景 | 不适合场景 |
| --- | --- | --- | --- |
| 询问 | 默认在写文件、跑命令等受控工具前请求批准。 | 不熟悉的仓库、高风险改动、生产相关操作、需要逐步确认的任务。 | 大量低风险重复操作,或你已经明确允许 Reasonix 连续执行。 |
| 自动 | 自动批准普通工具权限;显式 `ask` / `deny` 规则和计划确认仍生效。有界的新 project/reference 记忆可走安全 create-only 路径,其余记忆变更仍会询问。 | 日常读代码、改小问题、跑测试、可信工作区里的常规实现任务。 | 你希望每个写入或命令都人工确认的任务。 |
| Yolo | 跳过普通工具权限审批,让写文件和命令尽量连续执行;`deny` 规则、计划确认、ask 问题和强制审批仍不会被自动回答。 | 临时分支、可回滚工作区、已确认计划后的批量机械改动。 | 生产环境、敏感文件、删除/发布/推送等高风险操作,或需求边界不清时。 |
## 询问模式
询问模式是最保守的工具权限模式。Reasonix 遇到需要审批的工具调用时,会弹出审批卡片,你可以选择允许一次、本会话允许、总是允许或拒绝。
动态 Bash 都不能继承更宽的裸 `Bash`、前缀或 glob 规则。参数/算术展开、赋值、重定向、不含嵌套执行的 heredoc 与 glob 仍按普通模式 fallback;只有嵌套/间接执行在交互 Ask 与 Auto 中强制走人工审批。本会话与持久授权会把完全相同的整条命令保存为 `Bash=<literal>`。
### 审批卡片快捷键
- `←` / `→` 循环切换高亮动作。
- `Enter` 确认当前高亮的普通工具审批动作,默认高亮“允许一次”。
- `1` / `2` / `3` / `4` 可以选择对应编号的普通工具审批动作。
- 计划确认有三个直接动作:「开始执行 / 修改计划 / 暂不执行,退出计划模式」。桌面端点击一次或按对应数字键即可;CLI 可按对应数字键,或选中一行后按 `Enter`,并继续兼容使用 `n` / `Esc` 留在计划模式。退出操作会拒绝待处理计划并返回普通模式,不会启动执行回合。
- 除 CLI 正在等待计划确认的情况外,`Esc` 停止当前任务。
- 如果你用 `Tab` 聚焦到某个按钮,再按 `Enter` 会触发当前聚焦按钮本身,不会被高亮动作覆盖。
### 建议使用场景
- 第一次让 Reasonix 处理某个仓库或目录。
- 任务可能涉及删除文件、改配置、提交、发布、安装依赖或访问外部服务。
- 你想观察 Reasonix 准备调用什么工具,再决定是否继续。
- 你正在处理生产数据、密钥、隐私文件或不可轻易回滚的环境。
### 注意事项
- 询问模式更安全,但会增加交互次数。
- 询问不等于只读:批准普通 writer 后,写文件或命令仍会执行;已安装或明确授权的 MCP 不走逐调用审批。Sandbox 才是强制能力边界。
- 审批卡片里的“本会话允许”和“总是允许”是权限规则,不是切换到自动模式。
- 如果你已经确认任务范围很清楚,且操作都在可信工作区内,自动模式通常更顺手。
无头 `reasonix run` 没有可以回答的审批卡,因此默认询问姿态会对 writer fallback
和显式 `ask` 规则 fail closed,不会增加无法完成的确认,也不会静默放行。无人值守
自动化需要放行普通 writer 时,使用现有的 `--auto` / `-y`;不需要再学习额外的
安全设置。
## 自动模式
自动模式适合日常开发。它会把普通工具权限自动放行,减少反复点击审批;但它不是无限制执行。
自动模式仍会遵守这些边界:
- 显式 `deny` 规则仍然阻止对应工具或命令。
- 显式 `ask` 规则仍然会弹出审批。
- 计划模式的“开始执行”确认仍然需要你选择。
- 全局、偏好、feedback、更新、重复、敏感或超长的 `remember`,以及所有 `forget`
仍需要新鲜人工审批;Guardian/安全审查不能代替用户批准。新的、有界、非敏感
project/reference 事实可被判定为 create-only 并免提示保存。
- 嵌套或间接 Bash 即使处于获批计划执行窗口也需要人工审批,Guardian 与 hook allow 不能代替用户批准;普通展开、赋值、重定向和 glob 仍走 Auto 快速路径。
- 用户安装或明确授权的 MCP 直接执行,不再受这套逐调用模式影响;显式 `deny`、Plan 和严格只读子会话边界仍然生效。
- ask 问题仍然等待你回答,不会由自动模式代选。
### 建议使用场景
- 普通代码阅读、搜索、编辑和测试。
- 你信任当前工作区,且改动可以通过 Git 或测试回滚。
- 目标模式下希望 Reasonix 连续推进,但仍保留显式规则和关键确认。
- 计划模式已经确认方案,后续只是常规实现和验证。
### 自动模式什么时候会询问
自动模式是一种行为承诺,不是用户还要额外学习和配置的功能:
> Auto 自动执行权限策略允许的操作;只有遇到新计划、产品取舍或其他真正属于用户的决定时才询问。
- 工作区读写、命令、源码/配置/工作流编辑、依赖变更、测试和外部操作都按现有权限策略执行,不再由 Auto Guard 额外按风险弹窗。
- 因此,默认 Auto 下的 `git push`、发布或部署不会仅因属于外部操作而询问;用户配置的显式 `ask` / `deny`、sandbox、MCP 和工具专属权限仍然有效。
- 初次建立普通任务计划保持快速路径;当已有未完成的结构化任务计划被改写时,独立 reviewer 会比较旧计划、新计划和用户任务。合理的实现细化自动继续。真正的产品、策略或范围选择会显示一张中性的计划决策卡,列出移除和新增的步骤;「采用新计划并继续」会继续执行,「不采用,告诉 Auto 如何调整」只展开内联意见框,不会立即提交决定。
- 工具失败是执行可靠性信号,不是整个任务的权限边界。与失败操作无关的其他工作会直接继续,不经过 Auto Guard 确认或恢复 Reviewer;只有对同一精确失败操作的重试才进入有限恢复审查,真正的结构化计划选择仍是唯一会弹出用户决策卡的恢复路径。
- 超时只会获得简短的失败路径提示,要求先检查当前状态和部分执行结果再重试;不会要求用户重置 Auto、切换模式或重启会话。
- 失败后的诊断和恢复在固定的主机侧 Episode 预算内自动推进(无设置项):同一精确操作失败 3 次后只停止该操作;自最近一次真实进展后累计 6 次有效执行失败、累计 3 次 Reviewer 拒绝、或已停止操作被重复提交 3 次时,会停止后续 mutation/verification。改参数、命令或目标不能重置 Episode 总数。成功的 mutation 或主机识别的 verification 会清空无进展预算;普通诊断读取不会。
- Episode 硬停止后,主机确认的只读诊断仍可使用,后续 mutation 和 verification 会被隔离。随后 Auto 获得一次只总结的收尾机会,并以冷静的 `recovery_paused` 状态结束(不是发送失败)。下一条用户消息会自动开启新 Episode。
- “换个方案”、计划“开始执行”、真实的工具权限模式切换,以及新的普通用户消息,都会开启新 Episode。目标自动续跑与子 Agent 继承当前 Episode。显式的「继续任务」授权按 TaskScope 跨 Episode 保留。
- Reviewer 暂时不可用不会把普通恢复变成人工确认;若已检测到结构化计划转换,则直接把计划选择交给用户,避免 Auto 静默替用户决定。
- 无人值守环境遇到真正的计划决策时会 fail closed,不会静默代选。
- 无头 Ask/Auto/DontAsk 遇到嵌套或间接 Bash 也会 fail closed,除非已有完全相同的 `Bash=<literal>` 精确授权。
- 这些边界仅在自动模式下生效;Ask 与 YOLO 保持既有审批语义,也没有额外的安全开关需要理解。
自动模式不是文件系统快照或回滚机制。需要可恢复性时仍应使用干净 Git 分支或一次性 worktree。计划模式决定是否开始;开始后由 Auto 处理常规执行。
### 注意事项
- 自动模式不等于 Yolo。它不会跳过显式 `ask` 规则,也不会自动回答产品决策问题。
- 自动模式不会替代执行安全配置;希望拦截推送、发布、删除或生产操作时,应保留对应的 `deny` 或 `ask` 规则及 sandbox 边界。
- Auto Guard 不提供需要用户管理的写入工具白名单或重置流程;操作级停止仍然很窄,Episode 硬停止只暂停本轮自动恢复。基础能力边界仍由权限策略和 sandbox 决定。
- 如果你发现 Reasonix 反复准备做你不想要的操作,切回询问模式更容易干预。
## Yolo 模式
Yolo 模式用于最大化连续执行。开启后,普通工具审批会被跳过,写文件、运行命令等操作可以更少中断地执行。
Yolo 是唯一可以绕过嵌套/间接 Bash 人工审批要求的工具权限姿态;显式 `deny` 与 Sandbox 仍然生效。
### 怎么开启
- 直接在输入框下方选择 Yolo、把它设为新会话默认审批,或使用 **设置 → 快捷键**
中显示的当前绑定(默认 `Ctrl+Y` / `Cmd+Y`)切换。
- 直接选择“询问”或“自动”即可退出 Yolo。
- 通过快捷键进入 Yolo 时,Reasonix 会记住原来的“询问”或“自动”基线;再次按快捷键退出 Yolo,会恢复之前的基线模式。
### 建议使用场景
- 当前在临时分支或干净工作树,改坏了也能快速回滚。
- 任务已经在计划模式里确认过,后续只是按计划执行。
- 批量机械改动、格式化、补测试、跑一组本地验证命令。
- 你明确希望减少所有普通工具审批中断。
### 注意事项
- Yolo 只跳过工具权限审批,不会跳过计划确认和 ask 问题。
- Yolo 不会绕过后端的 `deny` 规则、系统权限、工作区限制或沙箱限制。
- 需要强制新鲜审批的记忆变更和计划确认仍然会等待你处理;只有符合安全策略的新 project/reference
create-only 事实可以免提示。MCP 授权发生在用户安装或项目配置声明时,不需要额外项目身份确认;
之后不会因为 writer 分类或 `destructiveHint` 再弹二次审批。显式 `deny`、Plan 与严格只读
子代理的工具过滤仍然生效。
- 不建议在生产环境、敏感目录、密钥文件、发布流程或需求不清楚的任务里开启。
## 与协作方式的组合
| 组合 | 行为 |
| --- | --- |
| 计划 + 询问 | 规划期间,普通内置工具仍按询问模式处理;MCP writer、destructive 目标与未授权 reader 会被 Plan 直接阻止。确认计划后,普通 writer fallback 自动放行,显式 `ask` / `deny` 与强制新鲜审批仍生效。 |
| 计划 + 自动 | 计划确认仍需要你批准;开始执行后,普通工具权限自动放行。 |
| 计划 + Yolo | 计划确认仍需要你批准;开始执行后,普通工具审批尽量不再打断。 |
| 目标 + 询问 | 目标会持续推进,但遇到工具审批会停下来等你。 |
| 目标 + 自动 | 适合大多数日常目标任务,连续推进且保留显式规则边界。 |
| 目标 + Yolo | 适合边界非常清楚、可回滚的目标任务;风险最高。 |
| 任一运行模式 + 任一工具权限 | 支持。运行模式影响工具面和执行合约,不改变工具审批语义。 |
## 推荐选择
- **不确定怎么选**:用“询问”。
- **日常开发**:用“自动”。
- **已确认计划、可回滚、想减少中断**:短时间使用“Yolo”。
- **高风险命令或敏感环境**:用“询问”,并通过 `deny` / `ask` 规则保护关键操作。
-110
View File
@@ -1,110 +0,0 @@
# 工具合约
<a href="./TOOL_CONTRACT.md">English</a>
本文记录 Reasonix 编译期内置工具的 provider-visible 合约。运行时 registry 使用同一条 canonical schema 路径;测试会校验这里列出的工具名、read-only 标记和 schema 快照不会漂移。
| 工具 | Read-only | 说明 |
| --- | --- | --- |
| `bash` | false | 执行 shell 命令并返回 stdout/stderr。构建、测试、git、包管理器等使用它;读写查找文件优先使用专用工具。 |
| `bash_output` | true | 读取后台 `bash` 或 `task` job 自上次读取后的新增输出和状态。 |
| `code_index` | true | 轻量内置代码符号索引;优先使用 `lsp_*` 或代码图 MCP,缺失时用它兜底。 |
| `complete_step` | true | 用证据记录已批准计划中一个步骤的完成情况。 |
| `compress` | true | 压缩当前模型可见对话中选定的范围,不删除可见历史。仅在用户明确要求压缩上下文时使用;锚点必须是某条真实用户消息中唯一、精确的原文片段。 |
| `delete_range` | false | 用精确 start/end 文本锚点删除文件中的连续范围。 |
| `delete_symbol` | false | 用 Go AST 删除 Go 源文件中的命名符号。 |
| `edit_file` | false | 将文件中的唯一精确字符串替换为另一个字符串。 |
| `glob` | true | 查找匹配 glob pattern 的文件。 |
| `grep` | true | 在文件或目录下按正则搜索文本。 |
| `kill_shell` | false | 终止后台 `bash` 或 `task` job。 |
| `ls` | true | 列出目录条目,可递归。 |
| `move_file` | false | 移动或重命名文件。 |
| `multi_edit` | false | 对单个文件原子应用多个编辑。 |
| `notebook_edit` | false | 编辑 Jupyter notebook 的单个 cell。 |
| `read_file` | true | 按可分页的行号格式读取文本文件。 |
| `recall` | true | 按压缩摘要「折叠工作索引」里的 #n 地址取回被折叠的对话片段;文件与代码应重新读取,recall 用于无法重建的内容(用户原话、不该重跑的命令输出)。每次压缩重置取回预算,超额整体拒绝而非截断。 |
| `todo_write` | true | 记录并替换当前工作的结构化任务列表。 |
| `wait` | true | 等待后台 job 完成并返回最终输出。 |
| `web_fetch` | true | 通过 HTTP/HTTPS 获取 URL 文本内容。 |
| `write_file` | false | 写入文件内容,必要时创建父目录。 |
## Schema 快照
完整 canonical schema 不在文档中手写,避免文档和代码手工漂移。运行:
```bash
go test ./internal/tool -run TestBuiltinToolContractDocumentation
```
该测试会用 `tool.BuiltinContractEntries` 校验每个内置工具都有文档行、read-only 标记、非空 description 和 canonical JSON schema。
## 默认 Full Boot Surface
默认 full-token boot 会发送上面的内置工具,并额外发送 session、memory、skill、subagent、LSP、install 和 slash-command 工具:
单模型均衡(Balanced)Executor 使用这套工具面。配置独立 Planner 的 Balanced 与全部交付优先
(Delivery)会在保留既有工具的同时增加一个稳定代理 `use_capability`(list/inspect/call/decline),
用于在不改变 provider 可见 Schema 的前提下发现和调用按需 MCP(含 `auto_start=false`)。Delivery
还会增加稳定执行合约,并由宿主运行时强制执行:变更和
验证命令必须先建立验收标准;变更后的工作必须完成复查、验证并通过带证据的 `complete_step` 签收;
Skill/MCP 的 require/prefer 路由受门禁约束(只读回答同样不能跳过 require 能力);中/高风险改动
强制结构化 review/security_review,且 `review_report` 的 `reviewed_paths` 必须有宿主观测到的
read/diff 证据。
双模型 Planner 与全部 task/fleet 子 Agent 同样使用 `use_capability`(且从不暴露直接
`mcp__*` schema)。Planner 与普通可写子 Agent 可调用已安装或项目配置 MCP,不要求
`readOnlyHint`;Planner 将 `destructiveHint` 留给 Executor,普通子 Agent 走可信 MCP 路径
(实时授权复核 + 仅显式 deny)。writer/destructive 调用仍会串行并按 mutation 记录,继续受
证据、工作区租约和 Delivery 门禁约束。严格只读子 Agent 共享同一代理 schema 与 Host 连接,但执行仍要求 `readOnlyHint` 且
非 destructive。Balanced 双模型会给 Planner 与 Executor 分别挂载独立代理 frontend,确保规划阶段
发现的 capability 在 handoff 后仍可直接调用;两者 ledger/audit 隔离,但共享 Host 连接。Economy
仍为单模型,不启用独立 Planner。
`use_capability` 的解析阶段无副作用:`action=list` 返回已配置 MCP 服务器的排序列表且不启动服务器;
对未连接服务器的 `action=call` 只生成惰性目标;Plan 只会对真实目标重新检查显式阶段 opt-out,服务器进程只在
权限门禁与 PreToolUse Hook 放行之后才启动。按需启动的
子进程随会话存活(不会随单次调用结束而退出);`action=inspect` 对已连接服务器列出实时工具,未连接
时只读取缓存 schema,绝不启动进程。无 schema 缓存的服务器首次发现走 `mcp-server:` id 的
`action=call`:解析为受门禁保护的连接目标(权限名为独立的
`mcp_connect__<server>`;例如精确拒绝规则 `deny = ["mcp_connect__github"]`
会在进程启动前拦截),放行后连接并返回实时工具目录。MCP 工具名规则仍为精确匹配,
`mcp__github__*` 不是工具名通配规则。安装 MCP 即授权 Planner 使用其非 destructive 工具;
第三方若错误省略 `destructiveHint`,远程副作用属于用户安装信任范围。每次 connect 或
`tools/call` 前,frontend 都会再次复核当前 runtime 的 enable、授权与精确 Host 连接身份;另一个
项目/tab 在共享 Host 上的同名 client 会在进程、网络或工具分发前被拒绝。
固定代理的 provider 可见 name、description、schema 与顺序不会随 MCP inventory 变化;但 Balanced
Executor 刻意保留直接 `mcp__*` 工具,因此安装、连接或刷新这些直接工具时,Executor 的整体 provider
前缀仍可能变化。
`ask`, `await_user`, `docs`, `explore`, `fleet`, `forget`, `history`, `install_skill`,
`install_source`,
`list_sessions`, `lsp_definition`, `lsp_diagnostics`, `lsp_hover`,
`lsp_references`, `memory`, `parallel_tasks`, `read_only_skill`,
`read_only_task`, `read_session`, `read_skill`, `read_subagent_result`, `remember`, `research`,
`review`, `run_skill`, `security_review`, `slash_command`, `task`.
`parallel_tasks` 与 `fleet` 会为每个已持久化子 Agent 返回公平分配的预览和稳定的
`Subagent reference`,使合并结果始终低于单工具输出上限。`read_subagent_result`
按 UTF-8 字节偏移分页读取某个引用对应的完整最终答案,因此长篇并行调研无需一次性全部
注入父会话也不会丢失。引用只允许在当前会话 lineage 和工作区内读取。
`use_capability`(`action` = `list` | `inspect` | `call` | `decline`)在所有执行设定
(`light` | `balanced` | `delivery`)下都出现在 provider 可见工具面。可选工具仍在 host
registry 中供调度,但不会展开到 top-level provider schema;模型通过 `use_capability`
调用,避免缓存前缀因 schema 变化而失效。
`internal/boot.TestBootToolContractMatchesProviderVisibleSurface` 会校验真实 boot registry 合约和 provider request 一致,包括 read-only 标记和 canonical schema。
## 统一启动工具面(所有执行设定)
三种执行设定共享同一套精简的 provider 可见核心:直接编码工具、后台 shell 生命周期工具,
以及稳定的能力代理:
`bash`, `bash_output`, `edit_file`, `kill_shell`, `read_file`, `wait`,
`write_file`, `compress` 与 `recall`(若注册),以及 `use_capability`。
可选工具(`glob`、`grep`、`ls`、`web_fetch`、MCP、skills、subagents、docs、会话历史、
记忆写入、workflow 等)仍在 host registry 中可调度;模型通过 `use_capability` 列举、
检查、调用或拒绝它们,且不会改变 provider 工具列表。执行设定改变的是规划 / 验证 /
独立复审策略,而不是 provider 可见工具集合。已退役的 `connect_tool_source` 不再注册。
-22
View File
@@ -1,22 +0,0 @@
# 用量 Catalog
Reasonix 将可丢弃的用量聚合保存到 `<cache root>/usage-catalog/v1.sqlite`。每日统计
JSONL 继续作为权威数据,并与旧版保持字节兼容。Catalog 记录文件 offset 和行 hash,
用于跨进程幂等,再派生按日、source、model/provider 的 rollup。
只有 JSONL append 成功后才会非阻塞提交 receipt;重复 receipt 不会重复累计。同一
offset hash 改变、文件缩短/替换、队列溢出或外部追加会触发 offset 对账或按日安全重建。
torn tail、空行、坏 JSON、旧 requests 默认值、时区、日期边界及 ActiveDays 语义都与
现有 JSONL decoder 一致。
只有所选日期全部完成索引时才使用 SQL;否则用户主动打开统计页时执行现有 JSONL 精确
查询,同时后台继续补齐。Catalog 故障不会阻塞 provider event、TurnDone、controller
启动或退出。
```sh
reasonix doctor catalogs [--json]
reasonix catalogs reindex usage [--json]
```
诊断只暴露 schema、完整性、lag、数量和错误,不包含模型请求内容;reindex 不修改每日
JSONL。
-4
View File
@@ -1,4 +0,0 @@
# Windows 10 LTSC 2019 crash investigation runbook (moved)
The diagnostics scope now covers all supported Windows and Linux environments.
Use the [cross-platform Desktop crash diagnostics runbook](./DESKTOP_CRASH_DIAGNOSTICS_RUNBOOK.md).
@@ -1,4 +0,0 @@
# Windows 10 LTSC 2019 崩溃调查运行手册(已迁移)
诊断范围现已覆盖所有受支持的 Windows 和 Linux 环境。请使用
[跨平台 Desktop 崩溃诊断运行手册](./DESKTOP_CRASH_DIAGNOSTICS_RUNBOOK.zh-CN.md)。
-59
View File
@@ -1,59 +0,0 @@
# Reasonix v5 Stable Production Checklist
Reasonix v5.9.9 is frozen as a stable release candidate. This checklist is the
release gate for PR #5217 and future v5-stable maintenance work.
## Release Gate
All checks below must pass before the v5 stable release candidate is merged.
## 1. Runtime Safety
- Sandbox isolation is verified.
- Resource budgets use ledger-based two-phase reservation and commit.
- No shared execution context can leak across sandboxed executions.
## 2. Control System Stability
- Distributed control plane is active and deterministic.
- Global equilibrium layer is deterministic.
- No single meta-controller is reintroduced.
## 3. Memory System Safety
- Causal compression is stable.
- Long-tail predictive and causal signal retention is validated.
- Truth-lock decay changes influence weight only, not correctness.
## 4. Predictive System Isolation
- Shadow observer remains read-only.
- Prediction-action bridge is advisory-only.
- Predictive warnings do not feed back into execution automatically.
## 5. Temporal System
- Dual logical and physical time reporting is visible.
- Lag and damping windows remain separated.
- Physical latency variance is not hidden by logical time normalization.
## 6. Architecture Freeze
- `system.StableSystemContract()` validates the v5.9.9 release boundary.
- `system.ArchitectureLocked` is enabled.
- v6-pre diagnostics remain observation-only.
## 7. Observability
- Trace and diagnostic systems remain non-invasive.
- Layer-collapse diagnostics do not affect runtime, prompts, provider requests,
or tool schemas.
## Final Release Flow
1. Validate the stable system contract.
2. Confirm the architecture lock is enabled.
3. Confirm v6-pre diagnostics are isolated.
4. Pass the production checklist.
5. Pass `system.ReleaseGuard()`.
6. Merge as the v5.9.9 stable release candidate.
-57
View File
@@ -1,57 +0,0 @@
# Reasonix v5 稳定版生产检查清单
Reasonix v5.9.9 已冻结为稳定发布候选版本。本清单是 PR #5217 以及后续 v5-stable 维护工作的发布门禁。
## 发布门禁
合并 v5 稳定发布候选版本前,以下检查必须全部通过。
## 1. 运行时安全
- sandbox 隔离已验证。
- 资源预算使用基于 ledger 的两阶段 reserve/commit。
- sandboxed execution 之间不会泄漏共享执行上下文。
## 2. 控制系统稳定性
- 分布式 control plane 已启用且具备确定性。
- global equilibrium layer 具备确定性。
- 不重新引入单点 meta-controller。
## 3. 记忆系统安全
- causal compression 保持稳定。
- long-tail predictive 和 causal signal retention 已验证。
- truth-lock decay 只改变影响权重,不改变事实正确性。
## 4. 预测系统隔离
- shadow observer 保持只读。
- prediction-action bridge 只输出 advisory。
- predictive warnings 不会自动反馈到 execution。
## 5. 时间系统
- logical time 和 physical time 的双时钟报告保持可见。
- lag window 和 damping window 保持分离。
- physical latency variance 不会被 logical time normalization 隐藏。
## 6. 架构冻结
- `system.StableSystemContract()` 能验证 v5.9.9 发布边界。
- `system.ArchitectureLocked` 已启用。
- v6-pre diagnostics 保持 observation-only。
## 7. 可观测性
- trace 和 diagnostic systems 保持 non-invasive。
- layer-collapse diagnostics 不影响 runtime、prompt、provider request 或 tool schema。
## 最终发布流程
1. 验证 stable system contract。
2. 确认 architecture lock 已启用。
3. 确认 v6-pre diagnostics 已隔离。
4. 通过 production checklist。
5. 通过 `system.ReleaseGuard()`。
6. 以 v5.9.9 stable release candidate 合并。
@@ -1,123 +0,0 @@
# Content-Driven Context Maintenance(Cache-Aware Checkpoint)
> 日期:2026-08-10
> 状态:当前实现说明(取代多阈值 prune/snip/native 自动维护叙述)
> 核心约束:canonical transcript 是永久事实源;唯一自动触发是 `compact_ratio`;缓存状态只影响成本与观测,不触发历史改写。
## 一、问题与目标
长会话需要同时满足:
1. 保留完整历史,以支持恢复、回退、分支和审计;
2. 在上下文接近上限时,构造更短且稳定的 provider-visible 请求;
3. 不因 cache TTL / cold resume 主动改写仍可命中的前缀。
旧路径使用 soft / snip / force 多阈值,并在压力下自动安装 prune 投影或调用 provider native compaction。该路径把维护成本与可恢复性缠在一起,也会在 resume 时破坏缓存前缀。
当前产品路径:
```text
canonical transcript (Session.Messages,普通维护永不改写)
|
+-- model-visible context projection / checkpoint
| stable prefix + one structured summary + recent tail
|
+-- first-visible tool bound (创建时 32KB Content/RawContent)
|
+-- cache state (warm/cold/unknown,仅成本与观测)
```
## 二、唯一自动触发
- 配置键:`agent.compact_ratio`(默认 `0.85`)
- 入口:`Prepare` / preflight 是唯一自动维护入口;`ObserveUsage` 只更新统计
- 不再存在自动 soft compact、tool_result snip/prune 投影、native multi-threshold 路径
- 兼容:旧配置键与 sidecar 字段可读;加载时清零/忽略/迁移删除,不新建 prune 投影
## 三、Checkpoint 形态
当 projected tokens ≥ `compact_ratio × context_window` 时,生成内容驱动 checkpoint:
```text
stable system / early prefix
-> 一条结构化 summary(单次摘要请求,上限约 16K)
-> recent tail(约 10% 窗口,夹在 32K–96K)
```
验收要点:
- 默认接受天花板约 50% 窗口;强制路径可放宽但不用不同 estimator 绕过
- 摘要失败不写 mechanical marker,不安装半成品,不改 canonical
- provider-visible 始终最多一条 summary;旧 summary 可进入下一次 fold 被滚动吸收
- 首次安装会预期 cache miss;安装后前缀应保持稳定以利后续 hit
## 四、持久化边界
### Canonical transcript
- `Session.Messages` 始终保存完整 transcript
- 普通 compaction、cold resume、旧 prune/snip API no-op 均不删除或替换 canonical 消息
- rewind / fork / branch 仍以 canonical 为事实源
### Context projection sidecar
- 路径:`<session>.context.json`(schema v3)
- 保存 projection、covered prefix fingerprint、version、prompt cache key、cache 状态与 telemetry
- 旧 prune / native 字段可加载后忽略;校验失败则安全重建
- 删除 session 时 sidecar 一并删除
## 五、运行时行为
### Resume
只根据 provider TTL 与最后活动时间记录 `warm` / `cold` / `unknown`。Resume 不调用 Compact、不安装 projection、不改写 tool results。
### Prepare
每次模型请求前:
1. 估计 projected tokens
2. 低于 `compact_ratio`:发送 append-only / 现有有效 projection
3. 达到阈值:至多一次 summary,CAS 安装 checkpoint
4. tool loop 中优先 notice,避免打断 tool 配对
### First-visible tool bound
工具结果创建时把模型可见 `Content` 限制在约 32KB,完整原文进 `RawContent` / archive。这是写入时策略,不是阈值触发的历史 snip 投影。
## 六、Provider 与输出预算
- 应用层 summary 是默认路径;Responses 等 native compaction 标记 unsupported 时回退 summary
- `max_output_tokens=0` 表示 auto ladder(普通 16K / reasoning 32K / high·max 64K);128K 仅显式配置
- auto ladder 与 `compact_ratio` 解耦
## 七、缓存影响
| 场景 | 预期 |
| --- | --- |
| warm resume 低于阈值 | 复用 append-only 前缀,无摘要 |
| 首次跨过 compact_ratio | 前缀变为 prefix+summary+tail,一次预期 miss |
| checkpoint 安装后继续对话 | 稳定 prefix 利于 hit;generation 作用域避免重复摘要 |
| cold resume | 只记 cache 状态,不因 TTL 重写历史 |
## 八、验证与烟雾
- 确定性:`internal/agent` compact / projection / prune no-op / restart 测试
- 离线 e2e:`benchmarks/context-maintenance-e2e` 的 `seed` + `resume`(`-offline`)
- 在线 e2e:同目录 `continue`(`DEEPSEEK_API_KEY`,`-max-usd` 费用上限,至多一次摘要)
## 九、有意保留的兼容层
不算功能缺口,也不声称“代码里已无旧概念”:
1. 配置结构体仍可读旧 soft/snip/force 键,加载时清零并迁移删除
2. sidecar 仍可解码旧 prune/native 字段后忽略
3. `PruneStaleToolResults` / `SnipStaleToolResults` 保留为 no-op API,避免旧调用点 panic
4. snip 几何 helpers 仍服务 first-visible 与 summary fold 输入,不再用于自动投影安装
## 十、明确未做
1. 重新启用多阈值自动 prune/snip 投影
2. 把 cache TTL 重新绑定到 transcript 改写
3. 跨 session 的 EventChain L2 自动恢复作为维护主路径
4. 完整 break-even 成本 dashboard
@@ -1,84 +0,0 @@
# 谁拥有一个运行中 Goal 的验收权
> 日期:2026-08-29
> 状态:设计契约,尚未实现。四条规则先钉死,测试围绕它们写。
> 定级:P1 correctness / low-frequency integrity boundary
## 已证明可达的状态
Goal 在声明 A 下启动,捕获 A 并随 goal 状态落盘;进程重启后新进程读到声明 B;恢复该 Goal 时旧判据被恢复进来。
```text
Goal frozen contract = A
current project declaration = B
```
证据:`internal/control/goal_resume_project_check_test.go`(全程只走 `Submit` 与 `resume` 两个生产入口,未对任何字段赋值)。同一进程内与 `--continue` 无 goal 两条路径均已在真机证伪,见 `benchmarks/project-check/README.md`。
**已证到的是两次 Goal 执行之间的外部声明变更。** agent 会改写声明、且改写能存活到下一进程,这一点 benchmark 单独证明过;把两半拼成"agent 自删考卷"合理但尚未在一次运行里证明。
## 当前两种实现都在替用户猜
```text
legacy: 看到 B → 自动忘掉 A
obligation: 看到 A 与 B → 自动要求 A + B
```
同一个状态既可能来自 agent 改写自己被考的判据(保留 A 才对),也可能来自 owner 在两次运行之间改了项目政策(A 可能确实该废止)。**现有 durable 状态不足以区分这两者**,所以任何自动策略都是在猜。
## 四条规则
1. **一个 Goal 拥有自己的冻结验收契约。** 创建时按 `VerificationIdentity` 集合冻结,随 Goal checkpoint 持久化,带 epoch。
2. **工作区声明的变化不会自动改变一个活跃 Goal 的验收权。** 声明变化之后只是"当前项目声明"。
3. **baseline ≠ current 产生的是 authority conflict,不是义务的自动并集。** 此时欠的不是"跑 A",而是"这个冲突尚未解决",finalization 因此受阻。
4. **只有显式的 host-owned rebase 能取代冻结契约,且取代不等于满足。** A 被 supersede 不使 B 通过;B 仍需在新 epoch 下有效的证据。
## 这把 provenance 难题换成了 capability 问题
不要去建立「改动由 agent 发起 → preserve,由人发起 → rebase」的判定。IDE 外部编辑、`git checkout`、后台进程、启动脚本、崩溃后未落盘的 receipt——provenance 在这些情形下都不可靠。
更可信的是一条边界:
> **文件写权限 ≠ authority 写权限。**
写 `REASONIX.md` 只改变工作区,不改变活跃 Goal 的冻结验收契约;owner 若确实想改这个 Goal 的验收,走显式的 Goal rebase 动作。这样 agent 自改与 owner 自改的模糊性一并消失,而且不需要知道是谁改的。
同理,V1 **不给 agent** 任何 `rebase_verification_contract` 能力。将来至多允许"请求",由 host/user 批准后才发生 epoch 转移。
## 最小状态机
```text
Goal starts → Epoch 1 {A}
│ restart, current declaration {B}
▼
Drift ── finalization blocked, authority unresolved
┌─────┴─────┐
Preserve Rebase
Epoch 1 {A} Epoch 2 {B},A superseded
│ │
verify A verify B
└─────┬─────┘
Complete
```
## 故意不做的
- **Merge(A+B 同时要求)**:没有 observed requirement,Preserve 与 Rebase 两个明确动作已足够。
- **把 `ObligationBaselineCheck` 直接升为最终权威**:它当前的形状隐含"A 一定继续有效",而可达性证明之后我们知道这不一定对。它应当逐步变成携带 baseline identities、current identities 与 goal epoch 的 drift 义务。
- **切换 `missingProjectChecks` 到 obligation derivation**:原来的迁移提案建立在"已确认 bypass"之上,那个前提已被更正。
## 随后要写的测试
| 测试 | 断言 |
| --- | --- |
| 可达性(已有) | frozen=A 与 current=B 生产可达 |
| drift 需要裁决 | 恢复后 finalization 受阻,理由是 authority unresolved,**既不是欠 A 也不是欠 B** |
| Preserve 能清债 | 裁决为 Preserve 后跑 A 即可完成——把 bypass 修成 deadlock 不算修好 |
| Rebase 能清债 | 裁决为 Rebase 后 A superseded、B 被欠,只跑 B 即可完成 |
| 不变量 | 没有显式 host rebase 时,工作区内容变更**永远**不能替换冻结契约 |
最后一条不需要知道文件是谁改的,因此它是这套设计里最稳的一条。
## 定级理由
不到 P0:没有高频或普通路径的证据。高于普通 P2:Goal durability 是明确支持的生产 lifecycle,而恢复后当前实现会**静默选择**一套验收权——静默更换正在执行任务的验收规则是 integrity 问题,不是边角情形。
-158
View File
@@ -1,158 +0,0 @@
# 子代理 handoff 协议基线(首次可测量)
> 日期:2026-08-29
> 状态:结案(就现有证据)。语义缺陷已修,未观察到模型行为缺陷。
> 前置条件:`3f02f9fc7` 注册 SubagentHandoff audit 通道,`3723ff454` 修好丢弃它的子代理 sink。在这两个提交之前,本文的数据在物理上无法采集——所有子代理 audit 都被一个只实现 `Emit` 的包装器吞掉了。
## 结案状态
| 层 | 判定 |
| --- | --- |
| 协议显著性 | **已解决**——14/14 判定子代理都提交了报告 |
| 报告可用性 | **未观察到实质问题**——malformed 仅 1/14 |
| 终止行为 | **已解决**——宿主真正关闭后,对照 0/14、实验 0/17 再无工具调用 |
| 宿主裁决词表 | **确认缺陷,已修**——`needs_work` 与 `closed` 曾被同一个 `accepted` 吞掉 |
| 结构性终止强制 | **现有证据不支持**——没有观察到关闭后的越界 |
准确的说法是:**修正了宿主语义,没有观察到模型行为缺陷。** `ChildClosureEpoch` 因此冻结——它会是防御性不变量,不是对已观察缺陷的修复;要立项应当以"交付场景不能依赖模型自觉"为理由,而不是以本文的数据。
## 研究问题
被委派的子代理,在没有任何额外提示压力的情况下,是否自然遵守 `complete_subtask` 协议?
## 结果
判定分母是 **Judged = `expected && exit == "completed"`**:被 provider 杀掉的子代理从未走到收尾那一步,把它算成拒绝会让分母回答另一个问题。
| 指标 | 结果 |
| --- | --- |
| Judged / Expected | 14/14 |
| Attempted / Judged | 14/14 |
| NeverAttempted / Judged | 0/14 |
| Accepted / Attempted | 14/14 |
| Malformed / Attempted | 1/14 |
| HostDowngrade / Accepted | 0/14 —— 见下方更正 |
| 报告后仍调用其他工具 | 10/14 |
| **重复调用 `complete_subtask`** | **11/14** |
| 报告真正结束了运行 | 3/14 |
调用次数分布 `{1:3, 2:4, 3:3, 4:1, 5:2, 6:1}`,且与运行长度单调相关:
```text
final_round= 2 → 1 次报告,之后 0 次工具调用
final_round= 7 → 3 次报告,之后 2 次
final_round=12 → 4 次报告,之后 5 次
final_round=16 → 5 次报告,之后 12 次
final_round=24 → 5 次报告,之后 12 次
```
## 更正(2026-08-29,同日)
上表两处需要更正,两处都由后续的 terminal-barrier 重放查出:
**一、`HostDowngrade 0/14` 只描述最后一份报告。** `observeSubagentHandoff` 取的是
`LatestCompletionReport()`,所以早先报告上的降级不可见。按全部 39 次被接受的调用重算:
**25 次被裁决为 `partial`,25 次输出含「the host lowered N unbacked criterion claim(s)」**,
只有 14 次维持 `complete`。审计字段本身没写错,但把它当作「子代理的声明都被证据支撑」会读反。
**二、「模型把报告当里程碑」这个解释被更强的证据取代。** 见下节。
## 解释
**措辞不是观测到的瓶颈。** 每一个被判定的子代理都尝试并最终产出了被接受的报告(14/14)。
重复调用不是模型误解了终止语义,而是**主机自己要求的**。首份报告的裁决结果与之后是否继续工作完全对应,14 个子代理零例外:
| | 首份报告被降级 | 未被降级 |
| --- | --- | --- |
| 之后继续工作 | 10 | 0 |
| 之后就此停止 | 0 | 4 |
`complete_subtask` 在降级时返回的正是:
> complete_subtask accepted: status=partial … — the host lowered N unbacked criterion claim(s); **run the check or cite what you really did**
而提示里的契约同时说:
> End this sub-task by calling complete_subtask **exactly once, as your final tool call**.
两句话互相矛盾:工具让子代理回去补检查再报一次,契约说只能报一次。模型服从了更晚、更具体的那一条,这是合理行为,不是协议失灵。`complete_subtask.go` 的注释说明这是刻意设计——「The submission always succeeds: the parent is better served by a claim it can see the host lower than by a rejection the parent never learns about」——但「总是 accepted」加上「去把检查跑了」,等于用 accepted 这个词表达了一次拒绝。
缺陷因此不是「契约缺少结构强制」,而是**裁决结果不是一个状态**:一份被降级的报告与一份原样通过的报告,返回的都是 `accepted`。
按事先约定的门槛(`Attempted / Judged ≥ 95%` 即不加 nudge),本轮结论是**不修改子代理提示**。
## Terminal-barrier 反事实(同一数据集的派生研究)
问题:子代理**首次被接受的** `complete_subtask` 是否已经是安全的终止点?
以首次被接受的报告为锚点,用宿主自己的分类器(`evidence.ToolCallMutationClass` / `CommandRunsVerification`)判定其后的每一次调用:
| 判定 | 子代理数 |
| --- | --- |
| PREMATURE_MUTATION(其后有路径可证的改动) | 1 |
| PREMATURE_OPAQUE_WORK(其后有范围不明的命令) | 7 |
| PREMATURE_VERIFICATION | 0 |
| SEMANTIC_REPORT_EVOLUTION(后续报告实质变化) | 2 |
| SAFE_REDUNDANT | 4 |
**结论:首次被接受即无条件终止是错的。** 它会在 10/14 的情况下截掉**宿主自己要求的**工作。
报告后的 47 次 bash 绝大多数是即席断言与巡查(`python3 -c "… assert …"`、`grep`、`git diff`、`[ … ] && echo PASS`),路径可证的真实改动只有 4 次且集中在一个子代理。宿主的 `CommandRunsVerification` 只认识测试运行器,认不出这些断言,所以 opaque 一栏是**未能分类工作的上界**,不是「它们写了东西」的断言。
两个未决的设计方向由此产生,本轮不做选择:让被降级的报告**返回拒绝而不是 accepted**(这样「exactly once」重新为真),或者引入一个由宿主裁决可关闭性的子代理纪元。
## closed / needs_work 实验臂(f508a0573)
把裁决说成状态之后,同一批语料重跑(12 个任务,`-budget 0`,无跳过,全部通过):
| | 对照(3723ff454) | 实验(f508a0573) |
| --- | --- | --- |
| 判定子代理 | 14 | 17 |
| 报告调用 | 40 | 41 |
| 每个子代理的 closed 次数 | —— | **恰好 1,17/17** |
| needs_work 裁决 | —— | 21 |
| **closed 之后的工具调用** | **0/14** | **0/17** |
| 多次报告 | 11/14 | 11/17 |
**结论是一个空结果,而这正是该记录的事情。** `closed` 之后零工具调用在两臂都成立——用同一个锚点(宿主首次说"完成了")重算对照即可看到。模型**本来就**遵守 closure 语义;之前看起来像"把报告当里程碑",是因为分析锚点定在首次**格式合法**的调用上,而宿主用同一个词回答了两种裁决。
所以这次改动买到的不是行为改变,而是三件事:裁决成为可读的计数(`closed` / `needs_work` 进入审计与指标,不必再回溯 trajectory);契约不再与工具输出矛盾;`closed` 成为每个子代理恰好一次的可识别事件——这是将来任何 ChildClosureEpoch 的前提。
`needs_work` 之后确实产生真实工作:21 次转换中,两次报告之间的工具调用数分布为 `{0:2, 1:6, 2:4, 3:6, 5:2, 7:1}`,**「零工作原样重报」只有 2/21**。
新的 recovery 归属显示:child 内 14 次、父循环 15 次,全部是 `missing_reasoning_model_silent`——provider 协议修复,**不是宿主把子代理拉回来**。这条归属让这句话有证据,而不是假设。
限制:17 个判定子代理、每任务单次;两臂 token 预算不同,任务通过率不可比;17 个里 14 个来自明确要求委派的提示。
证据:`<state-home>/research/subagent-closure-verdict/2026-08-29/`。
原始与派生证据:`<state-home>/research/subagent-terminal-barrier/2026-08-29/`,其 manifest 引用上游数据集的 manifest 哈希,不复制原始数据。
一个未能回答的问题:`protocol_recovery` 记录不带子代理 id,所以「后续工作是宿主恢复导致」这一类无法归因,判定集合里没有 HOST_RECOVERY。
## 限制
- 只有 14 个被判定的子代理;不适合做跨版本百分比比较。
- 其中 10 个来自被明确要求委派的任务。上述合规率是「子代理一旦存在」时的合规率,不能外推到模型自发委派的场景。
- read-only 子代理只有 3 个,且全部来自同一个 fanout 任务。「writer 因终止链更长而更容易漏掉最后一次转换」这一假设在现有数据中**未被支持**(两组 attempted/accepted 均为满分),但样本不足以否定它。
- 每个任务单次运行,语料本身是随机的。
## 单独登记的开放问题:自发委派率
同一批运行里,6 个未作要求的任务只产生了 1 个子代理,5 个明确要求委派的任务产生了 10 个。
这是一个独立的研究问题(委派策略),**不要和 handoff 终止语义混进同一个实验**。
## 原始证据
不进仓库:trajectory 按其包注释「与会话记录同等敏感」,四个批次合计约 3.2 MB。
```text
<state-home>/research/subagent-handoff/2026-08-29/
batch-01-fanout-pilot/ batch-03-delegation-next2/
batch-02-delegation-first7/ batch-04-delegation-last2/
manifest.json handoff.py
```
四个批次分开保存,因为预算截断本身就是 provenance:批次 02(800k 默认预算)和 03(1.2M)都被中止并跳过了剩余任务,批次 04 取消上限才跑完语料。`manifest.json` 钉住 revision、模型、判定口径与每个原始文件的 sha256;`handoff.py` 对任一批次目录重跑即可复现上表。
@@ -1,93 +0,0 @@
# AutoResearch Runtime Verification Matrix
> Superseded historical audit. The verified runtime described below is no
> longer active; Goal now owns budgeting, progress and completion directly.
Date: 2026-06-30
Scope: host-managed AutoResearch runtime, controller integration, desktop API,
frontend visibility, and focused end-to-end verification.
## Summary
AutoResearch has moved from a prompt convention to a host-managed runtime for
the implemented scope. The host now creates and resumes task state under
`.reasonix/autoresearch/<task-id>/`, injects a runtime summary into active goal
turns, records heartbeats and stale progress, records accepted evidence from
structured assistant evidence blocks, gates completion through default required
criteria, and exposes desktop/frontend status APIs with compact tab metadata.
The implementation is verified against the design requirements listed below.
The only residual limitation is that an external browser smoke test cannot call
the Wails `window.go` bridge directly; bridge behavior is covered by Go and
TypeScript tests, and the running Wails app was smoke-tested through `./dev`.
## Requirement Matrix
| Requirement | Evidence | Status |
| --- | --- | --- |
| Host creates task id and directory layout | `internal/autoresearch.Store.CreateTask`; `TestCreateTaskCreatesHostOwnedLayoutAndInitialState` | Verified |
| State files are host-owned JSON/JSONL under `.reasonix/autoresearch/<task-id>/` | `task_spec.json`, `progress.json`, `directions_tried.json`, `findings.jsonl`, `iteration_log.jsonl`, `heartbeat.jsonl`; store tests and `./dev` smoke task directory | Verified |
| Schema validation rejects missing required fields | `ValidateTask`; `TestValidateTaskReportsSchemaErrors` | Verified |
| JSONL append rejects invalid entries | `AppendFinding`; `TestAppendFindingRejectsInvalidEntry` | Verified |
| Accepted findings satisfy readiness | `Readiness`; `TestAppendFindingRecordsAcceptedEvidenceForReadiness`, `TestRecordEvidenceLinksFindingToCriterionAndSatisfiesReadiness` | Verified |
| Direction repetition increments stale count and pivot threshold | `RecordDirection`; `TestRecordDirectionIncrementsStaleAndRequiresPivot` | Verified |
| Controller computes stale/pivot instead of relying on prompt | `recordAutoResearchTurnProgress`; `TestResearchGoalTurnUpdatesAutoResearchStaleProgress` | Verified |
| Per-task concurrent writes are serialized | `Store.lockTask`; `TestConcurrentDirectionWritesAreSerializedPerTask` | Verified |
| `/goal --research` creates task and persists `AutoResearchTaskID` | `SetGoalWithResearchMode`; `TestResearchGoalCreatesHostManagedAutoResearchTask` | Verified |
| Explicit `.reasonix/autoresearch/<task-id>/` resumes existing task | `ResumeFromGoalText`; `TestResumeFromGoalTextLoadsExplicitTaskPath` | Verified |
| Missing explicit task path blocks instead of silently creating a new task | `ensureAutoResearchTask`; `TestResearchGoalMissingExplicitTaskBlocksInsteadOfCreatingNewTask` | Verified |
| Cold resume restores running AutoResearch task id | `goalMachine.restoreRunningFromState`; `TestResumeRestoresRunningAutoResearchGoalFromSidecar` | Verified |
| Compose injects host-generated `<autoresearch-runtime>` | `Controller.Compose`; `TestResearchGoalCreatesHostManagedAutoResearchTask`, resume test | Verified |
| Heartbeats are written around turns | `appendAutoResearchHeartbeat`; `TestResearchGoalTurnAppendsAutoResearchHeartbeats`; `./dev` smoke heartbeat log | Verified |
| Summary includes last heartbeat | `Summary`; `TestAppendHeartbeatRecordsDurableTurnStatus` | Verified |
| Completion is intercepted when readiness fails | `autoResearchReadinessFailure`; `TestResearchGoalCompletionIsInterceptedWhenReadinessFails` | Verified |
| Completion requires host-readable evidence for default criteria | `defaultAutoResearchSuccessCriteria`; `TestResearchGoalCreatesHostManagedAutoResearchTask`, `TestResearchGoalCompletionMarksAutoResearchTaskComplete` | Verified |
| Completion/blocked goal updates AutoResearch status | `finalizeAutoResearchTask`; `TestResearchGoalCompletionMarksAutoResearchTaskComplete`, `TestResearchGoalBlockedMarksAutoResearchTaskBlocked` | Verified |
| Host controller records structured evidence | `RecordAutoResearchEvidence`; `TestControllerRecordsAutoResearchEvidence` | Verified |
| Assistant replies can record structured evidence without changing tool schemas | `<autoresearch-evidence>` parser; `TestResearchGoalCompletionMarksAutoResearchTaskComplete` | Verified |
| AutoResearch evidence recording does not alter the default provider-visible tool surface | `TestAutoResearchEvidenceDoesNotChangeDefaultToolSurface`; boot tool contract tests | Verified |
| Desktop API exposes current/status/list/findings/open/record evidence | `desktop/app.go`; `TestAutoResearchStatusSurfaceForActiveTab`, `TestAutoResearchFindingsAreLoadedOnDemand`, `TestAutoResearchListReturnsWorkspaceTasks`, `TestAutoResearchOpenTaskRevealsTaskDirectory`, `TestAutoResearchRecordEvidenceThroughDesktopAPI` | Verified |
| Tab metadata includes compact summary only | `compactAutoResearch`; `TestAutoResearchStatusSurfaceForActiveTab` | Verified |
| Findings load on demand and cap by limit | `AutoResearchFindings`; `TestAutoResearchFindingsAreLoadedOnDemand` | Verified |
| AutoResearch does not appear as a configurable default status bar item | `statusbar-workspace.test.tsx` | Verified |
| Frontend bridge tolerates transient Wails IPC timing errors | `bridge-drag-rejection.test.ts` | Verified |
| Transcript lifecycle cards exist | Controller emits `event.Notice`; frontend renders notices via `NoticeCard`; `./dev` smoke shows `autoresearch task created` notice | Verified |
| `./dev` starts and AutoResearch creates durable state | Browser smoke against `http://127.0.0.1:34193`; task `20260630-065721-e2e-verify-autoresearch-ui-smoke-test` created | Verified |
## Commands Run
```bash
GOCACHE=/private/tmp/reasonix-go-build-cache go test ./internal/autoresearch ./internal/control -count=1
GOCACHE=/private/tmp/reasonix-go-build-cache go test . -run 'TestAutoResearch|TestMetaReportsGoalStatus' -count=1
GOCACHE=/private/tmp/reasonix-go-build-cache go test . -count=1
./node_modules/.bin/tsc --noEmit -p tsconfig.test.json
./node_modules/.bin/tsx src/__tests__/statusbar-workspace.test.tsx
pnpm test
./dev
```
`tsx` tests required running outside the sandbox because `tsx` creates IPC pipes
under `/var/folders`.
## End-to-End Smoke Notes
- `./dev` started successfully.
- Wails devserver: `http://127.0.0.1:34193`.
- External browser loaded the page with title `Reasonix`.
- Browser console had no error/warn entries.
- Submitted `/goal --research e2e verify autoresearch ui smoke test`.
- The transcript rendered `autoresearch task created:
20260630-065721-e2e-verify-autoresearch-ui-smoke-test`.
- Host state files were created under `.reasonix/autoresearch/...`.
- Cancelling the smoke goal wrote heartbeat warning `context canceled`.
- Known Wails dev terminal noise remains: `runtime:ready -> Unknown message from
front end: runtime:ready`.
## Remaining Risk
- The external browser smoke test cannot directly call Wails `window.go`; bridge
behavior is covered by Go/TS tests and the running Wails desktop app, but not
by direct browser-side method calls.
- The implementation intentionally avoids background daemon behavior; AutoResearch
advances only during normal Goal turns.
@@ -1,97 +0,0 @@
# AutoResearch Runtime Implementation Plan
> Superseded historical plan. The standalone runtime and desktop surfaces were
> later removed in favor of the unified Goal runtime.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Implement the host-managed AutoResearch runtime from the design spec, including durable state, controller integration, desktop API, and frontend visibility.
**Architecture:** Add `internal/autoresearch` as the source of truth for task state under `.reasonix/autoresearch/<task-id>/`. Goal controller code creates, resumes, summarizes, heartbeats, and gates completion through that package. Desktop and frontend read compact status metadata for the active tab and fetch heavier details on demand.
**Tech Stack:** Go standard library, existing `internal/control` goal FSM, desktop Wails bindings, React/TypeScript frontend, existing Go and frontend test runners.
---
## Implementation Tasks
### Task 1: AutoResearch Store And Schema
**Files:**
- Create: `internal/autoresearch/task.go`
- Create: `internal/autoresearch/store.go`
- Create: `internal/autoresearch/schema.go`
- Create: `internal/autoresearch/store_test.go`
- [ ] Write failing tests that prove `CreateTask` creates `.reasonix/autoresearch/<task-id>/state` and `logs`, writes `task_spec.json`, `progress.json`, empty JSONL files, and validates required fields.
- [ ] Run: `go test ./internal/autoresearch -run 'TestCreateTask|TestValidateTask' -count=1`
- [ ] Implement types, task id generation, atomic JSON writes, JSONL append validation, `CreateTask`, `LoadTask`, `ValidateTask`.
- [ ] Run: `go test ./internal/autoresearch -count=1`
### Task 2: Findings, Directions, Summary, Readiness
**Files:**
- Create: `internal/autoresearch/summary.go`
- Create: `internal/autoresearch/readiness.go`
- Modify: `internal/autoresearch/store.go`
- Modify: `internal/autoresearch/store_test.go`
- [ ] Write failing tests for `AppendFinding`, `RecordDirection`, stale/pivot thresholds, `Summary`, and `Readiness`.
- [ ] Run: `go test ./internal/autoresearch -run 'TestAppendFinding|TestRecordDirection|TestReadiness|TestSummary' -count=1`
- [ ] Implement accepted evidence lookup, direction fingerprint tracking, stale count updates, pivot requirement, readiness blocking, and compact summary generation.
- [ ] Run: `go test ./internal/autoresearch -count=1`
### Task 3: Goal Controller Runtime Integration
**Files:**
- Modify: `internal/control/goal.go`
- Modify: `internal/control/input.go`
- Modify: `internal/control/controller.go`
- Modify: `internal/control/turn_orchestrator.go`
- Modify: `internal/control/goal_test.go`
- Modify: `internal/control/input_test.go`
- [ ] Write failing tests that `/goal --research` creates or resumes an AutoResearch task and stores `AutoResearchTaskID` in persisted goal state.
- [ ] Write failing tests that `Compose` injects a host-generated `<autoresearch-runtime>` summary only in the current user turn.
- [ ] Write failing tests that goal completion is intercepted when AutoResearch readiness fails.
- [ ] Run targeted control tests for the new cases.
- [ ] Implement store wiring, create/resume, heartbeat hooks, runtime summary injection, and readiness intercept.
- [ ] Run: `go test ./internal/control -count=1`
### Task 4: Desktop API Surface
**Files:**
- Modify: `desktop/app.go`
- Modify: `desktop/tabs.go`
- Modify: `desktop/app_test.go`
- Modify: `desktop/tab_profile_test.go`
- [ ] Write failing tests for `AutoResearchStatus`, `AutoResearchCurrent`, `AutoResearchFindings`, and compact tab metadata.
- [ ] Implement view structs and methods that call `internal/autoresearch` without parsing prompt text.
- [ ] Ensure findings are capped and loaded on demand.
- [ ] Run: `go test ./desktop -run 'AutoResearch|MetaReports' -count=1`
### Task 5: Frontend Status And Detail UI
**Files:**
- Modify: `desktop/frontend/src/lib/statusBarItems.ts`
- Modify: `desktop/frontend/src/components/StatusBar.tsx`
- Modify: `desktop/frontend/src/components/ContextPanel.tsx`
- Modify: `desktop/frontend/src/bridge.ts`
- Modify: `desktop/frontend/src/__tests__/statusbar-workspace.test.tsx`
- Add frontend tests for AutoResearch detail rendering.
- [ ] Write failing frontend tests for hidden chip, running/pivot/blocked labels, opening the detail panel, and rendering loaded findings.
- [ ] Implement compact status chip, tooltip, detail section, refresh hooks, and on-demand findings loading.
- [ ] Run: `pnpm --dir desktop/frontend test`
### Task 6: End-To-End Verification
**Files:**
- Update generated Wails bindings if required.
- No broad refactors.
- [ ] Run: `go test ./internal/autoresearch ./internal/control ./desktop -count=1`
- [ ] Run: `pnpm --dir desktop/frontend test`
- [ ] Run the desktop app through the project dev command when feasible and manually verify a research goal shows the chip and panel state.
- [ ] Audit each design-spec goal against implementation and record remaining gaps before marking the goal complete.
@@ -1,25 +0,0 @@
# Billing currency refactor — acceptance checklist
## Automated (run before merge)
- [x] `go test ./...`
- [x] `cd desktop && go test ./...`
- [x] Key packages `-race` (billing, event, boot, cli, config, acp, serve, stats)
- [x] `cd desktop/frontend && pnpm typecheck && pnpm test:usage-stats && pnpm build`
- [x] `scripts/check-cache-impact.sh` → no provider-visible changes
- [x] `reasonix doctor billing` surfaces display/billing/FX-disabled/catalog
## Manual UI (Desktop + Serve)
1. **CNY ↔ USD hot switch** — Settings → Display currency; session estimate rebinds without zeroing; provider prices unchanged.
2. **Restart restore** — reload a session with `*.telemetry.json` v2 legacy cost; ledger migrates lazily; no invented totals for wiped zeros.
3. **Mixed models** — main + planner + subagent with different currencies; total uses shared valuations or shows incomplete.
4. **No runtime FX** — no ECB request, cache, or refresh goroutine; custom-currency totals stay in original buckets; official dual-region still values via `official_table`.
5. **Wallet** — multi-currency balance shows `≈` total + detail tooltip (originals, rate date).
6. **MiMo Token Plan** — cost label uses PAYG-equivalent wording when `billing_mode=subscription_equivalent`.
7. **Narrow / restore** — Serve and Desktop after session resume show same selected estimate.
## GitHub follow-up
- [ ] Open PR; link #4565, #3527, #4546; note supersedes #7790
- [ ] After merge, mark PR #7790 superseded
@@ -1,481 +0,0 @@
# AutoResearch Runtime Design
> Superseded: the active AutoResearch runtime was removed when Goal became the
> sole runtime. This document is retained as historical design context only;
> new Goal runs do not create or mutate `.reasonix/autoresearch/` state.
## Context
Reasonix already has Goal mode and AutoResearch instructions. When a goal looks
long-running, `activeGoalBlock` injects an AutoResearch protocol that asks the
model to create `.reasonix/autoresearch/<task-id>/` and maintain files such as
`task_spec.json`, `progress.json`, `findings.jsonl`, `directions_tried.json`, and
`heartbeat.jsonl`.
The current behavior is useful, but the durable state is mostly prompt-driven.
The host does not create the task directory, validate schemas, compute
`stale_count`, require pivots, expose a structured status API, or provide a real
resume mechanism. This design upgrades AutoResearch from a prompt convention to
a host-managed runtime.
## Goals
- Host creates and owns the AutoResearch task id and directory layout.
- Host validates task state with typed schemas.
- Host records heartbeat, progress, findings, directions tried, and iteration log.
- Host computes stale and pivot pressure from accepted evidence and direction
repetition.
- Goal completion is blocked until required success criteria have evidence.
- Existing AutoResearch task ids can be resumed by the controller.
- The desktop/API layer can query AutoResearch status without parsing prompt text.
## Non-Goals
- No large desktop panel in the first implementation.
- No autonomous background daemon. AutoResearch advances only through normal Goal
turns.
- No parallel writable sub-agent redesign in this feature.
- No network, publish, payment, credential, or destructive-operation bypass.
Existing Reasonix gates still apply.
## Proposed Architecture
Add a new package:
```text
internal/autoresearch/
task.go
store.go
schema.go
summary.go
readiness.go
```
The package is responsible for all filesystem state under:
```text
.reasonix/autoresearch/<task-id>/
state/
task_spec.json
progress.json
directions_tried.json
findings.jsonl
iteration_log.jsonl
logs/
heartbeat.jsonl
```
The controller remains the owner of Goal lifecycle. AutoResearch state is a
durable sidecar attached to a running Goal when research mode is on or auto
research is triggered.
## Core Types
`TaskSpec`:
```json
{
"task_id": "20260629-153000-debug-lag",
"goal": "Find the root cause of UI event-loop lag and verify the fix",
"scope": ["desktop/frontend", "desktop"],
"non_goals": [],
"allowed_operations": {
"write": true,
"network": false,
"publish": false
},
"success_criteria": [
{
"id": "root_cause",
"description": "A reproducible root cause is identified",
"required": true,
"evidence_ids": []
}
]
}
```
`Progress`:
```json
{
"status": "running",
"iteration": 4,
"current_direction": "profile markdown rendering",
"stale_count": 1,
"pivot_count": 0,
"blocked_reason": "",
"updated_at": "2026-06-29T15:30:00Z"
}
```
`Finding` JSONL entries:
```json
{
"id": "f1",
"kind": "test",
"summary": "A markdown render benchmark reproduces the lag",
"source": "command",
"command": "pnpm --dir desktop/frontend test",
"paths": ["desktop/frontend/src/components/MarkdownRenderer.tsx"],
"accepted": true,
"created_at": "2026-06-29T15:30:00Z"
}
```
`DirectionTried` entries record normalized direction fingerprints so the host can
detect repeated work:
```json
{
"fingerprint": "profile-markdown-rendering",
"summary": "Profile markdown rendering",
"first_seen_iteration": 2,
"last_seen_iteration": 4,
"count": 2
}
```
## Store API
The first implementation should expose a small host API:
```go
type Store struct { /* workspace root + autoresearch root */ }
func (s *Store) CreateTask(goal string, opts CreateOptions) (*Task, error)
func (s *Store) LoadTask(taskID string) (*Task, error)
func (s *Store) ResumeFromGoalText(goal string) (*Task, bool, error)
func (s *Store) AppendHeartbeat(taskID string, h Heartbeat) error
func (s *Store) AppendFinding(taskID string, f Finding) error
func (s *Store) RecordDirection(taskID string, d Direction) (*Progress, error)
func (s *Store) UpdateProgress(taskID string, patch ProgressPatch) (*Progress, error)
func (s *Store) ValidateTask(taskID string) (*ValidationReport, error)
func (s *Store) Readiness(taskID string) (*ReadinessReport, error)
func (s *Store) Summary(taskID string) (*Summary, error)
```
All writes should be atomic: write to a temp file in the same directory, fsync
where practical, then rename. JSONL appends should validate each entry before
writing.
## Controller Integration
When Goal mode starts:
1. If research mode is off, behavior is unchanged.
2. If AutoResearch is on, the controller creates a task unless the goal contains
an explicit `.reasonix/autoresearch/<task-id>/` path.
3. If an explicit task path exists, the controller loads and validates that task.
4. The active goal state stores `AutoResearchTaskID`.
Before each AutoResearch turn:
- The controller appends a heartbeat with `status=starting_turn`.
- The composed user input includes a concise host-generated summary:
task id, status, iteration, current direction, stale count, pivot count,
blockers, open success criteria, and next required runtime action.
- The static prompt still explains the protocol, but host state is authoritative.
After each turn:
- The controller appends a heartbeat with `status=turn_done`.
- If tools ran, it records basic iteration metadata.
- If no accepted evidence was recorded, or the same direction repeated, the host
increments `stale_count`.
- At `stale_count >= 2`, the next turn summary requires a structural pivot.
- At `stale_count >= 4`, the goal is blocked unless the agent asks for the
smallest external input needed.
The model may still write detailed notes, but host-owned JSON files are the
source of truth.
## Completion Gate
When the model emits `[goal:complete]`, the controller runs AutoResearch
readiness before normal Goal completion:
- `task_spec.json` and `progress.json` must exist and validate.
- Every required success criterion must have at least one accepted evidence id.
- Evidence ids must resolve to entries in `findings.jsonl`.
- If code was changed, there must be accepted verification evidence or an
explicit accepted reason why verification could not run.
- If `progress.status` is blocked, completion is rejected.
- If `stale_count > 0`, completion is allowed only when the final iteration added
accepted evidence that addresses the stale direction.
Failure returns a concrete intercept message to the model listing missing
criteria and required next actions.
## Resume Behavior
AutoResearch resume has two paths:
- Explicit: a goal or prompt includes `.reasonix/autoresearch/<task-id>/`.
- Session-sidecar: the persisted Goal state contains `AutoResearchTaskID`.
On resume, the host validates the task. If state is corrupt, it blocks execution
with a repair message instead of silently asking the model to infer state.
## Desktop/API Surface
The first UI implementation should make AutoResearch visible without turning the
chat surface into a project-management app. It should use the existing desktop
layout patterns: status bar for compact state, side panels for inspectable
details, and transcript cards for turn-local events.
Host methods:
- `AutoResearchStatus(taskID string)`
- `AutoResearchList()`
- `AutoResearchCurrent()`
- `AutoResearchFindings(taskID string, limit int)`
- `AutoResearchOpenTask(taskID string)`
The status payload should include:
- task id
- goal
- status
- iteration
- current direction
- stale count
- pivot count
- last heartbeat
- finding count
- open success criteria
- blocker
`AutoResearchOpenTask` opens `.reasonix/autoresearch/<task-id>/` in the
workspace panel or OS file browser, matching existing workspace reveal behavior.
## Deferred Desktop UI Design
The runtime PR exposes the desktop API and compact tab metadata first. The
default frontend tool/status surface intentionally stays unchanged until the UI
entry points below are implemented and reviewed as a separate product decision.
### Entry Points
AutoResearch should appear in three places:
1. Status bar chip: a compact always-visible indicator when the active tab has a
running or resumable AutoResearch task.
2. Context/side panel section: an inspectable task summary for the active tab.
3. Transcript event cards: lightweight markers for task creation, pivot required,
blocked, resumed, and completed.
This keeps the primary chat workflow intact while making durable research state
visible and recoverable.
### Status Bar Chip
Add an `autoresearch` status item to the existing status bar item system. It is
hidden when no AutoResearch task is active for the current tab.
Display states:
- `Research 4` for running iteration 4.
- `Pivot` when `pivot_required` is true.
- `Blocked` when status is blocked.
- `Done` briefly after completion.
The chip should include an icon, short label, and tooltip. The tooltip contains:
- task id
- current direction
- stale count
- open criteria count
- last heartbeat age
Clicking the chip opens the AutoResearch detail panel.
### Detail Panel
Add a compact AutoResearch section to the right-side context/workspace area. The
panel should be dense and operational, not decorative.
Header:
- task id
- status
- iteration
- last heartbeat
- open task folder button
Summary rows:
- goal
- current direction
- stale count
- pivot count
- blocker
Success criteria list:
- criterion description
- required/optional marker
- evidence count
- status: open, satisfied, blocked
Findings list:
- newest accepted findings first
- kind badge: command, file, test, benchmark, manual, review, verification
- summary
- source command/path if present
- created time
Controls:
- Resume: starts or continues `/goal --research .reasonix/autoresearch/<task-id>/`
for the active tab when not running.
- Pause: clears active Goal continuation without deleting task state.
- Open Folder: reveals the task directory.
- Copy Task ID: copies the task id.
The first implementation can omit inline editing of task spec fields. Task state
is owned by the host and model workflow; UI edits would need validation and a
separate audit path.
### Transcript Cards
Emit lightweight notices or typed events for important AutoResearch lifecycle
changes:
- task created
- task resumed
- heartbeat failed
- pivot required
- readiness blocked completion
- task completed
- task blocked
The transcript should not render the full runtime summary every turn. It should
show only meaningful lifecycle changes, because the full summary is already
available in the detail panel and injecting it visually every turn would add
noise.
### Frontend State Flow
Extend bridge/types with:
```ts
interface AutoResearchStatusView {
taskId: string;
goal: string;
status: "running" | "blocked" | "complete" | "stopped" | "invalid";
iteration: number;
currentDirection: string;
staleCount: number;
pivotCount: number;
pivotRequired: boolean;
lastHeartbeatAt: string;
findingCount: number;
openCriteria: AutoResearchCriterionView[];
blocker: string;
taskPath: string;
}
```
`MetaForTab` should include only the small active-task summary needed to render
the status chip. The heavier findings list should be loaded on demand through
`AutoResearchFindings`, so normal chat turns do not pull large JSONL data into
frontend state.
Refresh strategy:
- Refresh current AutoResearch status after `turn_done`.
- Refresh when switching tabs.
- Refresh when receiving an AutoResearch lifecycle event.
- Do not poll every second. The heartbeat is persisted for durability, not for a
live dashboard animation.
### Empty and Error States
- No active task: hide the chip and show no panel section by default.
- Invalid state: show an `Invalid` status with the exact validation error and an
Open Folder action.
- Missing task folder on resume: show a blocked state and keep the Goal from
silently continuing.
- Stale state: show the pivot requirement prominently but do not mark it as an
error.
### Accessibility and Layout
- Use existing button, tooltip, panel, and status bar patterns.
- Keep the status chip width stable so the status bar does not shift during
streaming.
- Long goals and directions should wrap in the panel but truncate in the chip
tooltip label.
- Findings should be keyboard navigable and copyable.
- Use icons only where the existing design system already uses them; avoid a
marketing-style card layout.
## Error Handling
- Missing task directory: create only when starting a new task; otherwise return
a clear resume error.
- Corrupt JSON: block AutoResearch continuation and surface the exact file.
- Schema mismatch: return validation errors with paths and fields.
- Failed heartbeat write: warn and continue for non-critical writes, but block
completion if state cannot be validated.
- Concurrent writes: serialize per task id inside the store.
## Testing
Unit tests:
- task id generation is stable in shape and collision-safe
- store creates the expected directory layout
- schema validation rejects missing required fields
- JSONL append rejects invalid entries
- direction repetition increments stale count
- pivot threshold produces a pivot requirement
- readiness blocks missing evidence
- readiness accepts complete criteria with accepted findings
- resume loads explicit task id from goal text
Controller tests:
- AutoResearch goal creates task state
- active goal block includes host-generated summary
- every turn appends heartbeat
- `[goal:complete]` is intercepted when readiness fails
- explicit `.reasonix/autoresearch/<task-id>/` resumes existing state
Desktop/API tests:
- app method returns current AutoResearch status for the active tab
- tab metadata includes only compact AutoResearch summary
- findings are loaded on demand and capped by limit
- status chip hides when no task is active
- status chip opens the detail panel
- detail panel renders running, pivot, blocked, invalid, and complete states
- Open Folder calls the existing reveal/open path behavior
- tab switch refreshes the visible AutoResearch summary
## Rollout
Phase 1: implement `internal/autoresearch` store, schemas, summary, readiness,
and tests.
Phase 2: integrate with Goal controller create/resume/heartbeat/summary and
completion intercept.
Phase 3: add desktop/API status methods, status bar chip, detail panel, transcript
lifecycle cards, and frontend tests.
Phase 4: optionally let tools or a dedicated host tool record structured
findings directly, reducing reliance on model-authored JSON.
## Compatibility and Cache Impact
This design should not change provider-visible tool schemas in phase 1. The
active goal prompt changes only when AutoResearch is active. Cache impact is
therefore low for ordinary sessions and medium for AutoResearch sessions because
the injected runtime summary changes each turn.
No existing `.reasonix/autoresearch` task should be deleted or rewritten without
validation and explicit migration logic.
@@ -1,243 +0,0 @@
# Spatiotemporal Composability (Plugin/Runtime v2)
## Context
Reasonix already has a solid extension kernel: `Contribution → RuntimeSnapshot`,
`RuntimeSet` reverse/idempotent cleanup with generation guards, Builder freeze
and activate stages, and `Rebuild` that keeps the old controller on failure.
This design ports four established runtime-composition mechanisms into that
kernel without adopting an external runtime or changing the implementation
language:
1. Unified runtime effect scopes
2. Typed, reactive dependency graph
3. Generation / epoch component lifecycle
4. Verifiable atomic reload (build → activate → publish → drain)
## Fixed decisions
- Upgrade native plugins to `reasonix.io/plugin/v2`
- Reject v1 and legacy native manifests (no apiVersion) at install, doctor, boot
- Keep existing stdio/HTTP transport; do not rewrite the transport layer
- Do not introduce an external runtime dependency
- Irreversible external work uses effect receipts; never fake rollback
- Receipt recovery is process-local, bounded, and does not promise crash recovery
- Preserve system prompt, tool schema, and memory prefix cache-first constraints
- Claude compatibility plugin paths are out of scope for v2 native runtime changes
## Lifecycle
Component states:
```text
Inactive → Preparing → Active → Draining → Inactive
Failed (activation or cleanup failure; retains error + receipts)
```
Component granularity for v1 of this design: one native v2 sidecar runtime
package is one component node. Host-owned providers, interceptors, and UI
bindings are child contributions of that node. Individual tools are not
independent lifecycle nodes yet.
## EffectScope
`RuntimeSnapshot` stays immutable configuration. Live handles live only in an
`EffectScope` (default implementation held by `RuntimeSet`):
| Class | Meaning |
| --- | --- |
| Reversible | Dispose undoes the effect |
| Cancelable | Cancel stops further work; wait for completion on dispose |
| Compensatable | Dispose may run a compensate function |
| Irreversible | Record receipt only; never claim rollback success |
Rules:
- Each effect runs dispose at most once
- Within a component: reverse registration order
- Across components: reverse topological drain order
- Independent components may activate/clean in parallel
- Mid-activation failure auto-disposes already tracked effects
- Dispose waits for cancelable background work
- Cleanup errors become diagnostics; they never skip remaining dispose
Resources that must track through EffectScope: sidecar process, MCP
client/transport, provider stream, watcher, event subscription, UI hub binding,
goroutine/background job, session temporary resources, controller cleanup
callback.
## Snapshot / plan / controller boundary
```text
RuntimeSnapshot = immutable configuration and dependency view
RuntimeSet = live resources owned by one generation (EffectScope)
RuntimePlan = transition from old snapshot to new snapshot
Controller = owner of the published generation
```
Live handles (process, connection, watcher, callback, goroutine) must never
enter `RuntimeSnapshot`.
## Capability contract
`internal/extensioncontract` is a leaf package shared by `pluginpkg` and
`internal/extension`:
- `CapabilityKey{Namespace, Kind, ID}` — always namespaced
- `Capability{Key, Version, SchemaHash}` — stable schema hash required for
provider/tool/UI capabilities
- `Requirement` extends capability with `VersionRange` and `Optional`
Versions use `golang.org/x/mod/semver`. Key, kind, version, and schema hash
participate in canonical hashing.
## Manifest v2
Native `reasonix-plugin.json` requires:
```json
{
"apiVersion": "reasonix.io/plugin/v2",
"name": "example",
"version": "2.0.0",
"requires": [...],
"provides": [...],
"runtime": { "command": "...", "intercepts": [], "replaces": [], "capabilities": [] }
}
```
Validation:
- Missing / v1 / unknown major apiVersion → hard reject
- `provides` is the capability ceiling for handshake
- Handshake must not declare undeclared capabilities
- Declared-but-missing capabilities become `Unavailable` (no forge)
- Non-optional missing requirements keep component `Inactive`
- Optional missing requirements allow activation with diagnostics
- Required dependency cycles fail with full cycle path
- Multiple providers for the same `(namespace, kind, id)` need explicit
selection or report conflict
- Replacement slots stay single-owner; interceptors stay additive by priority
Claude/Codex compatibility manifests are unchanged.
## Dependency graph and RuntimePlan
Build flow:
```text
Discover → Parse → Validate → Resolve capabilities → Validate versions/schema
→ Detect cycles → Topological activate order → Reverse drain order
```
Deterministic sort keys: dependency rank, scope rank, priority, canonical
component ID.
Epoch identity for a consumer:
```text
epoch = [capability key, provider component ID, provider version, provider schema hash]
```
Only epoch changes force consumer reload.
`RuntimePlan` carries Added / Removed / Reloaded / Unchanged plus ActivateOrder
and DrainOrder. Diagnostics record `PrefixChanged` only after comparing the old
and new snapshot `CacheHash`, while `ProviderChanged` records provider capability
add/remove/reload independently. No-op plans must not change `CacheHash`.
Changes should affect only the relevant subgraph (provider, MCP server,
interceptor chain, UI hub).
## Atomic publish / drain
```text
Discover → ResolveGraph → CreatePlan → Preflight
→ ActivateNewGeneration → AwaitReady → PublishController → DrainOldGeneration
```
- Preflight failure: old runtime untouched
- New activation failure: dispose entire new generation scope
- Do not publish until new runtime is Active
- Publish is a single atomic pointer swap
- After publish: old controller Draining (no new turns; finish in-flight)
- Drain timeout cancels remainder and records receipts
- Stale generation UI / provider stream / event output is silently dropped
## Effect receipts
Irreversible and compensatable effects record owner, generation, component,
class, timestamps, receipt id, and compensation status. Provider requests
already submitted must not be reported as rolled back. File writes need prior
state or compensate. Sent messages get receipts and duplicate-send protection.
Cancellation stops later work only.
The ledger retains at most 32 generations and 256 receipts per generation.
Eviction marks recovery evidence incomplete, releases associated file priors,
and prevents a clean-rollback claim. The ledger is not persisted; process-crash
recovery is explicitly outside this design's scope.
## Permissions
The dependency graph answers what capabilities a component may obtain. It does
not replace the sandbox: trusted host components, native sidecars (process
boundary), and untrusted plugins still face permission interceptors at call
time.
## Error reasons (extension protocol)
In addition to the existing frozen table, v2 adds:
- `dependency_unsatisfied`
- `dependency_cycle`
- `schema_mismatch`
- `activation_failed`
- `stale_generation`
- `cleanup_failed`
(`unsupported_version` already exists.)
## Migration
- `reasonix plugin migrate <name> --to-v2` rewrites only pre-extension native
manifests that omit `apiVersion`, backs up the original, and errors on
dependencies it cannot infer. It does not accept Manifest v1.
- `reasonix plugin doctor` reports dependency and protocol errors
- v1 / missing apiVersion native manifests fail install, doctor, and boot
## Acceptance
1. Every v2 component resource has a clear owner and EffectScope
2. Activation failure never leaks new-generation resources
3. Missing dependencies become diagnostic Inactive, never panics
4. Dependency replacement reloads only the affected subgraph
5. Publish/drain order is testable
6. Irreversible work is never marked rollback-success
7. v1 native manifests are rejected without dual-read or migration
8. Prompt/tool cache stability guards pass
9. Doctor explains why a component is inactive
10. No external runtime or language dependency
## Implementation status (repo)
| Area | Status |
| --- | --- |
| EffectScope / LiveScope / RuntimeSet | Done |
| extensioncontract + DependencyGraph + RuntimePlan | Done |
| Manifest `reasonix.io/plugin/v2` + reject v1 | Done |
| Extension Protocol major v2 + schema/SDK gen | Done |
| PublishGate + stale UI/provider chunk drop | Done |
| Sidecar incremental adopt (StartPackagesWithPlan) | Done |
| RebuildFrom + desktop/CLI lastBuildResult | Done |
| MCP Host ReplaceServerBackend stable proxy | Done |
| Provider liveClient re-resolve | Done |
| LifecycleRegistry + FormatRuntimeStatus | Done |
| Manifest provides ceiling on handshake | Done |
| Full subgraph-only BuildRuntime (skip tools/prompt) | Done: plan classify + ReuseAssembly rediscovery skip + CacheHash reuse |
| Controller admission on PublishGate | Done (`turnDroppedDraining`) |
| Lifecycle metrics + graph/plan benchmarks | Done (soft CI baselines) |
| EffectScope for every MCP/watcher/UI resource | Done (activator + session-resources fold + Track* helpers) |
| Receipt-driven recovery / checkpoint | Done (`DecideResume` + doctor/desktop + provider-submit receipts) |
| Drain timeout product path | Done (`ScheduleDrainWatch` / `SweepAndForceExpire`) |
| Doctor runtime + desktop RuntimeDoctor | Done |
| Integration matrix (rapid reload, provider classify, admission) | Done |
-204
View File
@@ -1,204 +0,0 @@
package productdocs
import (
"encoding/json"
"fmt"
"strings"
)
type localizedText struct {
English string `json:"en"`
Chinese string `json:"zh"`
}
type releaseItem struct {
Kind string `json:"kind"`
Level string `json:"level"`
Title localizedText `json:"title"`
Body localizedText `json:"body"`
Href string `json:"href"`
Refs []int `json:"refs"`
}
type releaseRecord struct {
Version string `json:"version"`
Date string `json:"date"`
Channel string `json:"channel"`
Status string `json:"status"`
Title localizedText `json:"title"`
Summary localizedText `json:"summary"`
Surfaces []string `json:"surfaces"`
Guides []releaseItem `json:"guides"`
Highlights []releaseItem `json:"highlights"`
Upgrade []releaseItem `json:"upgrade"`
Risks []releaseItem `json:"risks"`
Contributors []string `json:"contributors"`
Changes struct {
New []releaseItem `json:"new"`
Improved []releaseItem `json:"improved"`
Fixed []releaseItem `json:"fixed"`
} `json:"changes"`
}
type renderedReleaseDocument struct {
path string
source string
locale string
version string
content string
}
func renderReleaseDocuments(data []byte) ([]renderedReleaseDocument, int, error) {
var catalog struct {
Releases []releaseRecord `json:"releases"`
}
if err := json.Unmarshal(data, &catalog); err != nil {
return nil, 0, fmt.Errorf("decode release-notes/releases.json: %w", err)
}
if len(catalog.Releases) == 0 {
return nil, 0, fmt.Errorf("release-notes/releases.json contains no releases")
}
seen := make(map[string]struct{}, len(catalog.Releases))
documents := make([]renderedReleaseDocument, 0, len(catalog.Releases)*2)
for _, release := range catalog.Releases {
if !validReleaseVersion(release.Version) {
return nil, 0, fmt.Errorf("release-notes/releases.json has invalid version %q", release.Version)
}
if _, duplicate := seen[release.Version]; duplicate {
return nil, 0, fmt.Errorf("release-notes/releases.json has duplicate version %q", release.Version)
}
seen[release.Version] = struct{}{}
for _, locale := range []string{"en", "zh-CN"} {
suffix := ".md"
if locale == "zh-CN" {
suffix = ".zh-CN.md"
}
documents = append(documents, renderedReleaseDocument{
path: "changelog/v" + release.Version + suffix,
source: "release-notes/releases.json#v" + release.Version,
locale: locale,
version: release.Version,
content: renderReleaseMarkdown(release, locale),
})
}
}
return documents, len(catalog.Releases), nil
}
func validReleaseVersion(version string) bool {
if version == "" || strings.Contains(version, "..") {
return false
}
for _, r := range version {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '-' {
continue
}
return false
}
return true
}
func renderReleaseMarkdown(release releaseRecord, locale string) string {
zh := locale == "zh-CN"
localize := func(value localizedText) string {
if zh {
return strings.TrimSpace(value.Chinese)
}
return strings.TrimSpace(value.English)
}
heading := func(en, cn string) string {
if zh {
return cn
}
return en
}
var b strings.Builder
fmt.Fprintf(&b, "# Reasonix v%s — %s\n\n", release.Version, localize(release.Title))
if zh {
fmt.Fprintf(&b, "发布日期:%s\n\n发布渠道:%s\n\n状态:%s\n", release.Date, release.Channel, release.Status)
} else {
fmt.Fprintf(&b, "Release date: %s\n\nChannel: %s\n\nStatus: %s\n", release.Date, release.Channel, release.Status)
}
if len(release.Surfaces) > 0 {
fmt.Fprintf(&b, "\n%s: %s\n", heading("Surfaces", "涉及产品面"), strings.Join(release.Surfaces, ", "))
}
fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Summary", "摘要"), localize(release.Summary))
renderReleaseItems(&b, heading("Highlights", "亮点"), release.Highlights, localize, heading)
if len(release.Changes.New)+len(release.Changes.Improved)+len(release.Changes.Fixed) > 0 {
fmt.Fprintf(&b, "\n## %s\n", heading("Changes", "变更"))
renderReleaseItemGroup(&b, heading("New", "新增"), release.Changes.New, localize, heading)
renderReleaseItemGroup(&b, heading("Improved", "改进"), release.Changes.Improved, localize, heading)
renderReleaseItemGroup(&b, heading("Fixed", "修复"), release.Changes.Fixed, localize, heading)
}
renderReleaseItems(&b, heading("Upgrade notes", "升级说明"), release.Upgrade, localize, heading)
renderReleaseItems(&b, heading("Known risks", "已知风险"), release.Risks, localize, heading)
renderReleaseItems(&b, heading("Related guides", "相关指南"), release.Guides, localize, heading)
if len(release.Contributors) > 0 {
fmt.Fprintf(&b, "\n## %s\n\n%s\n", heading("Contributors", "贡献者"), strings.Join(release.Contributors, ", "))
}
return strings.TrimSpace(b.String()) + "\n"
}
func renderReleaseItems(
b *strings.Builder,
title string,
items []releaseItem,
localize func(localizedText) string,
heading func(string, string) string,
) {
if len(items) == 0 {
return
}
fmt.Fprintf(b, "\n## %s\n", title)
renderReleaseItemBullets(b, items, localize, heading)
}
func renderReleaseItemGroup(
b *strings.Builder,
title string,
items []releaseItem,
localize func(localizedText) string,
heading func(string, string) string,
) {
if len(items) == 0 {
return
}
fmt.Fprintf(b, "\n### %s\n", title)
renderReleaseItemBullets(b, items, localize, heading)
}
func renderReleaseItemBullets(
b *strings.Builder,
items []releaseItem,
localize func(localizedText) string,
heading func(string, string) string,
) {
for _, item := range items {
title := localize(item.Title)
body := localize(item.Body)
if title == "" && body == "" {
continue
}
fmt.Fprintf(b, "\n- **%s**", title)
if item.Kind != "" {
fmt.Fprintf(b, " [%s]", item.Kind)
} else if item.Level != "" {
fmt.Fprintf(b, " [%s]", item.Level)
}
if body != "" {
fmt.Fprintf(b, ": %s", body)
}
if item.Href != "" {
fmt.Fprintf(b, " [%s](%s)", heading("Open guide", "打开指南"), item.Href)
}
if len(item.Refs) > 0 {
refs := make([]string, 0, len(item.Refs))
for _, ref := range item.Refs {
refs = append(refs, fmt.Sprintf("#%d", ref))
}
fmt.Fprintf(b, " (%s: %s)", heading("Refs", "关联"), strings.Join(refs, ", "))
}
b.WriteByte('\n')
}
}
-11
View File
@@ -1,11 +0,0 @@
// Package releasenotes exposes the reviewed release catalog bundled with each
// Reasonix build.
package releasenotes
import "embed"
// Content contains the structured release history from the exact source
// revision used to build the product.
//
//go:embed releases.json
var Content embed.FS
File diff suppressed because it is too large. Load diff
@@ -1,34 +0,0 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const files = execFileSync("git", ["ls-files", "README.md", "docs/*.md", "site/src/pages/*", "site/src/pages/**/*"], {
encoding: "utf8",
}).trim().split("\n").filter(Boolean);
const migrationReferences = new Set([
"docs/RELEASING.md",
"docs/SIGNPATH_WINDOWS_ADMIN_SOP.md",
"docs/CLI.md",
"docs/CLI.zh-CN.md",
]);
const rules = [
[/release-channel-switch/, "public release channel selector"],
[/reasonix upgrade (?:preview|stable)\b/i, "deprecated channel install command"],
[/[?&]channel=(?:preview|canary)\b/i, "public channel deep link"],
[/npm (?:i|install).*(?:@canary|@next)\b/i, "public prerelease npm install"],
[/\bStable 1\.x\b|· stable\b|current 1\.x stable\b/i, "public Stable channel label"],
];
const failures = [];
for (const file of files) {
if (migrationReferences.has(file) || file.includes("/changelog/")) continue;
const source = readFileSync(file, "utf8");
for (const [pattern, description] of rules) {
if (pattern.test(source)) failures.push(`${file}: ${description}`);
}
}
if (failures.length) {
console.error(`single-release public contract failed:\n${failures.join("\n")}`);
process.exit(1);
}
console.log(`single-release public contract OK (${files.length} files checked)`);
-244
View File
@@ -1,244 +0,0 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import { loadCatalog, upsertRelease, validateCatalog } from "./release-notes.mjs";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const apiBase = process.env.DEEPSEEK_API_BASE || "https://api.deepseek.com";
const model = process.env.DEEPSEEK_MODEL || "deepseek-v4-pro";
function parseArgs(argv) {
const values = {};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}`);
values[arg.slice(2)] = argv[++index];
}
return values;
}
function runGit(args) {
return execFileSync("git", args, { cwd: repoRoot, encoding: "utf8" }).trim();
}
function normalizeVersion(version) {
return String(version || "").replace(/^(?:desktop-|npm-)?v/, "");
}
function repositoryName() {
if (process.env.GITHUB_REPOSITORY) return process.env.GITHUB_REPOSITORY;
const remote = runGit(["remote", "get-url", "origin"]);
const match = remote.match(/github\.com[/:]([^/]+\/[^/.]+)(?:\.git)?$/);
if (!match) throw new Error("cannot determine GitHub repository; set GITHUB_REPOSITORY");
return match[1];
}
function commitRange(from, to) {
return runGit(["log", "--first-parent", "--format=%H%x09%s%x09%b%x00", `${from}..${to}`])
.split("\0")
.map((record) => record.trim())
.filter(Boolean)
.map((record) => {
const [sha, subject, ...body] = record.split("\t");
return { sha, subject, body: body.join("\t").trim() };
});
}
function prNumbersFromCommits(commits) {
const refs = new Set();
for (const commit of commits) {
for (const match of `${commit.subject}\n${commit.body}`.matchAll(/#(\d+)/g)) refs.add(Number(match[1]));
}
return [...refs];
}
async function githubJson(path, { allowMissing = false } = {}) {
const headers = { Accept: "application/vnd.github+json", "User-Agent": "reasonix-release-notes" };
if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN || process.env.GH_TOKEN}`;
}
const response = await fetch(`https://api.github.com${path}`, { headers, signal: AbortSignal.timeout(30_000) });
if (allowMissing && response.status === 404) return null;
if (!response.ok) throw new Error(`GitHub API ${path} failed: ${response.status}`);
return response.json();
}
async function collectPullRequests(repository, commits) {
const numbers = new Set(prNumbersFromCommits(commits));
const associated = await Promise.all(
commits.map((commit) => githubJson(`/repos/${repository}/commits/${commit.sha}/pulls`, { allowMissing: true })),
);
for (const pulls of associated) for (const pull of pulls || []) numbers.add(pull.number);
const pulls = await Promise.all([...numbers].map((number) => githubJson(`/repos/${repository}/pulls/${number}`, { allowMissing: true })));
return pulls.filter(Boolean).map((pull) => ({
number: pull.number,
title: pull.title,
body: String(pull.body || "").slice(0, 2000),
author: pull.user?.login || "",
labels: (pull.labels || []).map((label) => label.name),
}));
}
function collectDocLinks(from, repository, to) {
const linkRef = runGit(["rev-parse", to]);
const paths = runGit(["diff", "--name-only", `${from}..${to}`])
.split("\n")
.filter((path) => /^(?:docs|README)[/\w.-]*\.(?:md|mdx)$/i.test(path));
return paths.map((path) => `https://github.com/${repository}/blob/${linkRef}/${path}`);
}
function assertGroundedRefs(value, allowedRefs, path = "release") {
if (Array.isArray(value)) {
value.forEach((item, index) => assertGroundedRefs(item, allowedRefs, `${path}[${index}]`));
return;
}
if (!value || typeof value !== "object") return;
if (Array.isArray(value.refs)) {
for (const ref of value.refs) {
if (!allowedRefs.has(ref)) throw new Error(`${path}.refs contains PR #${ref}, which is outside the release range`);
}
}
for (const [key, child] of Object.entries(value)) assertGroundedRefs(child, allowedRefs, `${path}.${key}`);
}
function extractJson(content) {
if (!content?.trim()) throw new Error("DeepSeek returned empty content");
const parsed = JSON.parse(content);
return parsed.release || parsed;
}
async function askDeepSeek(payload, retry = true) {
const key = process.env.DEEPSEEK_API_KEY;
if (!key) throw new Error("DEEPSEEK_API_KEY is required");
const response = await fetch(`${apiBase.replace(/\/$/, "")}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({
model,
// Release notes need deterministic structured output, not hidden chain of
// thought. Thinking tokens share the generation budget with content and
// can otherwise leave response_format JSON empty or truncated.
thinking: { type: "disabled" },
temperature: 0,
max_tokens: 8000,
response_format: { type: "json_object" },
messages: [
{
role: "system",
content: `You are Reasonix's release editor. Return one JSON object with a \"release\" property. Write factual, user-facing product release notes in equivalent English and Simplified Chinese. Group changes by user outcome, not by commit. Never invent capabilities, migrations, risks, PR numbers, contributors, URLs, or metrics. Every highlight and change must cite one or more supplied PR numbers. Use this exact release shape:
{
\"version\": \"semver\", \"date\": \"YYYY-MM-DD\", \"channel\": \"stable|prerelease\",
\"title\": {\"en\":\"\",\"zh\":\"\"}, \"summary\": {\"en\":\"\",\"zh\":\"\"},
\"surfaces\": [\"desktop\"],
\"guides\": [{\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"href\":\"https://...\"}],
\"highlights\": [{\"kind\":\"new|improved|fixed|security\",\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
\"changes\": {\"new\":[],\"improved\":[],\"fixed\":[]},
\"upgrade\": [{\"level\":\"info|warning\",\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
\"risks\": [{\"title\":{\"en\":\"\",\"zh\":\"\"},\"body\":{\"en\":\"\",\"zh\":\"\"},\"refs\":[123]}],
\"contributors\": [], \"links\": {\"github\":\"https://...\",\"compare\":\"https://...\",\"download\":\"https://...\"}
}
Return guides only for supplied documentation URLs. Mention upgrade action or risk only when explicitly supported; otherwise use empty arrays. Output JSON only.`,
},
{ role: "user", content: `Create the release record from these public GitHub sources:\n${JSON.stringify(payload)}` },
],
}),
});
if (!response.ok) throw new Error(`DeepSeek API failed: ${response.status} ${await response.text()}`);
const data = await response.json();
const choice = data.choices?.[0];
try {
return extractJson(choice?.message?.content);
} catch (error) {
if (!retry) {
throw new Error(`${error.message} (finish_reason=${choice?.finish_reason || "unknown"})`);
}
return askDeepSeek(payload, false);
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (!args.version) throw new Error("--version is required");
const version = normalizeVersion(args.version);
const catalog = await loadCatalog();
const channel = version.includes("-") ? "prerelease" : "stable";
const baseVersion = version.split("-")[0];
const previousRecord = channel === "stable"
? catalog.releases.find((release) => release.version !== version && release.channel === "stable")
: catalog.releases.find(
(release) =>
release.version !== version &&
release.channel === "prerelease" &&
release.baseVersion === baseVersion,
) || catalog.releases.find((release) => release.channel === "stable");
const previous = args.from || previousRecord?.version;
if (!previous) throw new Error("--from is required when no previous release exists");
const previousVersion = normalizeVersion(previous);
const previousIsPreview = previousVersion.includes("-");
const from = previous.match(/^(?:desktop-|npm-)?v/)
? previous
: previousIsPreview
? `v${previousVersion}`
: `desktop-v${previousVersion}`;
const to = args.to || "HEAD";
const repository = repositoryName();
const commits = commitRange(from, to);
if (!commits.length) throw new Error(`no commits found in ${from}..${to}`);
const pulls = await collectPullRequests(repository, commits);
if (!pulls.length) throw new Error(`no pull requests found in ${from}..${to}`);
const docLinks = collectDocLinks(from, repository, to);
const date = args.date || new Date().toISOString().slice(0, 10);
const tag = args.tag || (channel === "prerelease" ? `v${version}` : `desktop-v${version}`);
const source = {
version,
date,
channel,
range: `${from}..${to}`,
pullRequests: pulls,
documentationUrls: docLinks,
};
const release = await askDeepSeek(source);
release.version = version;
release.releaseId = version;
release.baseVersion = baseVersion;
release.date = date;
release.channel = source.channel;
release.status = "reviewed";
release.previousRelease = previousVersion;
const previewOrdinal = version.match(/-preview\.([1-9][0-9]*)$/)?.[1];
if (channel === "prerelease") {
if (!previewOrdinal) throw new Error("Preview release version must use MAJOR.MINOR.PATCH-preview.N");
release.builds = {
cli: `v${version}`,
desktop: `v${baseVersion}-preview.${previewOrdinal}`,
npm: `${baseVersion}-canary.${previewOrdinal}`,
};
} else {
release.builds = {
cli: `v${version}`,
desktop: `v${version}`,
npm: version,
};
}
release.contributors = [...new Set(pulls.map((pull) => pull.author).filter(Boolean))];
release.links = {
github: `https://github.com/${repository}/releases/tag/${tag}`,
compare: `https://github.com/${repository}/compare/${from}...${tag}`,
download: channel === "prerelease"
? "https://reasonix.io/?download=desktop&channel=preview#start"
: "https://reasonix.io/?download=desktop&channel=stable#start",
};
release.guides = (release.guides || []).filter((guide) => docLinks.includes(guide.href));
assertGroundedRefs(release, new Set(pulls.map((pull) => pull.number)));
validateCatalog({ schemaVersion: 1, releases: [release] });
await upsertRelease(release);
console.log(`Generated bilingual release notes for v${version} from ${pulls.length} pull request(s).`);
}
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
-80
View File
@@ -1,80 +0,0 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { loadCatalog, releaseForVersion } from "./release-notes.mjs";
function invariant(condition, message) {
if (!condition) throw new Error(message);
}
function parseArgs(argv) {
const [command = "validate", ...rest] = argv;
const values = { command };
for (let index = 0; index < rest.length; index += 1) {
const arg = rest[index];
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}`);
values[arg.slice(2)] = rest[++index];
}
return values;
}
export function validateReleaseEvent(event, release) {
invariant(event && typeof event === "object" && !Array.isArray(event), "release event must be an object");
invariant(event.schemaVersion === 1, "release event schemaVersion must be 1");
invariant(event.releaseId === release.version, "release event releaseId does not match reviewed notes");
invariant(
event.channel === (release.channel === "prerelease" ? "preview" : "stable"),
"release event channel does not match reviewed notes",
);
invariant(/^[0-9a-f]{40}$/.test(event.candidateSha), "release event candidateSha must be a full commit SHA");
if (release.candidateSha) {
invariant(event.candidateSha === release.candidateSha, "release event candidateSha does not match reviewed notes");
}
invariant(/^\d{4}-\d{2}-\d{2}T/.test(event.publishedAt), "release event publishedAt must be an ISO timestamp");
invariant(event.releaseNotesUrl === `https://reasonix.io/changelog/v${release.version}/`, "release event URL is invalid");
invariant(event.builds && typeof event.builds === "object", "release event builds are required");
for (const surface of ["cli", "desktop", "npm"]) {
invariant(typeof event.builds[surface] === "string" && event.builds[surface], `release event builds.${surface} is required`);
if (release.builds?.[surface]) {
invariant(event.builds[surface] === release.builds[surface], `release event builds.${surface} does not match reviewed notes`);
}
}
return event;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
invariant(args.version, "--version is required");
const catalog = await loadCatalog(args.catalog ? resolve(args.catalog) : undefined);
const release = releaseForVersion(catalog, args.version);
if (args.command === "generate") {
invariant(args.sha, "generate requires --sha");
invariant(args.output, "generate requires --output");
const event = validateReleaseEvent({
schemaVersion: 1,
releaseId: release.version,
channel: release.channel === "prerelease" ? "preview" : "stable",
candidateSha: args.sha,
publishedAt: args["published-at"] || new Date().toISOString(),
releaseNotesUrl: `https://reasonix.io/changelog/v${release.version}/`,
builds: release.builds,
}, release);
await writeFile(resolve(args.output), `${JSON.stringify(event, null, 2)}\n`);
return;
}
invariant(args.command === "validate", `unknown command ${args.command}`);
invariant(args.input, "validate requires --input");
const event = JSON.parse(await readFile(resolve(args.input), "utf8"));
validateReleaseEvent(event, release);
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
-316
View File
@@ -1,316 +0,0 @@
#!/usr/bin/env node
import { readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const defaultCatalogPath = resolve(repoRoot, "release-notes/releases.json");
const localizedFields = ["title", "body"];
const changeKinds = ["new", "improved", "fixed"];
const itemKinds = new Set(["new", "improved", "fixed", "security"]);
const releaseChannels = new Set(["stable", "prerelease"]);
const releaseStatuses = new Set(["reviewed", "published"]);
function invariant(condition, message) {
if (!condition) throw new Error(message);
}
function isObject(value) {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
function validateLocalized(value, path) {
invariant(isObject(value), `${path} must be an object`);
for (const lang of ["en", "zh"]) {
invariant(typeof value[lang] === "string" && value[lang].trim(), `${path}.${lang} must be a non-empty string`);
}
}
function validateRefs(refs, path) {
if (refs === undefined) return;
invariant(Array.isArray(refs), `${path} must be an array`);
for (const ref of refs) invariant(Number.isInteger(ref) && ref > 0, `${path} contains invalid PR number ${ref}`);
}
function validateItem(item, path, { kind = false, href = false, level = false } = {}) {
invariant(isObject(item), `${path} must be an object`);
for (const field of localizedFields) validateLocalized(item[field], `${path}.${field}`);
if (kind) invariant(itemKinds.has(item.kind), `${path}.kind is invalid`);
if (href) invariant(typeof item.href === "string" && /^https:\/\//.test(item.href), `${path}.href must be HTTPS`);
if (level) invariant(item.level === "info" || item.level === "warning", `${path}.level is invalid`);
validateRefs(item.refs, `${path}.refs`);
}
function semverParts(version) {
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
invariant(match, `invalid version ${version}`);
return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] || ""];
}
export function compareVersionsDesc(a, b) {
const aa = semverParts(a);
const bb = semverParts(b);
for (let i = 0; i < 3; i += 1) {
if (aa[i] !== bb[i]) return bb[i] - aa[i];
}
if (aa[3] === bb[3]) return 0;
if (!aa[3]) return -1;
if (!bb[3]) return 1;
return String(bb[3]).localeCompare(String(aa[3]), "en", { numeric: true });
}
export function validateCatalog(catalog) {
invariant(isObject(catalog), "catalog must be an object");
invariant(catalog.schemaVersion === 1, "catalog.schemaVersion must be 1");
invariant(Array.isArray(catalog.releases) && catalog.releases.length > 0, "catalog.releases must not be empty");
const versions = new Set();
for (const [index, release] of catalog.releases.entries()) {
const path = `releases[${index}]`;
invariant(isObject(release), `${path} must be an object`);
semverParts(release.version);
invariant(!versions.has(release.version), `duplicate version ${release.version}`);
versions.add(release.version);
invariant(/^\d{4}-\d{2}-\d{2}$/.test(release.date), `${path}.date must use YYYY-MM-DD`);
invariant(releaseChannels.has(release.channel), `${path}.channel is invalid`);
if (release.releaseId !== undefined) {
invariant(release.releaseId === release.version, `${path}.releaseId must equal version`);
}
if (release.baseVersion !== undefined) {
semverParts(release.baseVersion);
invariant(!release.baseVersion.includes("-"), `${path}.baseVersion must be stable semver`);
invariant(
release.version === release.baseVersion || release.version.startsWith(`${release.baseVersion}-`),
`${path}.baseVersion does not match version`,
);
}
if (release.status !== undefined) {
invariant(releaseStatuses.has(release.status), `${path}.status is invalid`);
}
if (release.candidateSha !== undefined) {
invariant(/^[0-9a-f]{40}$/.test(release.candidateSha), `${path}.candidateSha must be a full commit SHA`);
}
if (release.previousRelease !== undefined) {
semverParts(release.previousRelease);
invariant(release.previousRelease !== release.version, `${path}.previousRelease must differ from version`);
}
if (release.builds !== undefined) {
invariant(isObject(release.builds), `${path}.builds must be an object`);
for (const surface of ["cli", "desktop", "npm"]) {
invariant(
typeof release.builds[surface] === "string" && release.builds[surface].trim(),
`${path}.builds.${surface} must be a non-empty string`,
);
}
}
if (release.status !== undefined) {
for (const field of ["releaseId", "baseVersion", "previousRelease", "builds"]) {
invariant(release[field] !== undefined, `${path}.${field} is required for managed release records`);
}
if (release.channel === "stable") {
invariant(release.version === release.baseVersion, `${path}.stable version must equal baseVersion`);
invariant(release.builds.cli === `v${release.version}`, `${path}.builds.cli does not match Stable version`);
invariant(release.builds.desktop === `v${release.version}`, `${path}.builds.desktop does not match Stable version`);
invariant(release.builds.npm === release.version, `${path}.builds.npm does not match Stable version`);
} else {
const preview = release.version.match(/^(\d+\.\d+\.\d+)-preview\.([1-9][0-9]*)$/);
invariant(preview, `${path}.managed prerelease must use MAJOR.MINOR.PATCH-preview.N`);
invariant(preview[1] === release.baseVersion, `${path}.Preview baseVersion does not match version`);
invariant(release.builds.cli === `v${release.version}`, `${path}.builds.cli does not match Preview version`);
invariant(release.builds.desktop === `v${release.version}`, `${path}.builds.desktop does not match Preview version`);
invariant(
release.builds.npm === `${release.baseVersion}-canary.${preview[2]}`,
`${path}.builds.npm does not match Preview ordinal`,
);
}
}
validateLocalized(release.title, `${path}.title`);
validateLocalized(release.summary, `${path}.summary`);
invariant(Array.isArray(release.surfaces) && release.surfaces.length > 0, `${path}.surfaces must not be empty`);
invariant(new Set(release.surfaces).size === release.surfaces.length, `${path}.surfaces contains duplicates`);
invariant(Array.isArray(release.guides), `${path}.guides must be an array`);
release.guides.forEach((item, itemIndex) => validateItem(item, `${path}.guides[${itemIndex}]`, { href: true }));
invariant(Array.isArray(release.highlights) && release.highlights.length > 0, `${path}.highlights must not be empty`);
release.highlights.forEach((item, itemIndex) => validateItem(item, `${path}.highlights[${itemIndex}]`, { kind: true }));
invariant(isObject(release.changes), `${path}.changes must be an object`);
for (const changeKind of changeKinds) {
invariant(Array.isArray(release.changes[changeKind]), `${path}.changes.${changeKind} must be an array`);
release.changes[changeKind].forEach((item, itemIndex) =>
validateItem(item, `${path}.changes.${changeKind}[${itemIndex}]`),
);
}
invariant(Array.isArray(release.upgrade), `${path}.upgrade must be an array`);
release.upgrade.forEach((item, itemIndex) => validateItem(item, `${path}.upgrade[${itemIndex}]`, { level: true }));
invariant(Array.isArray(release.risks), `${path}.risks must be an array`);
release.risks.forEach((item, itemIndex) => validateItem(item, `${path}.risks[${itemIndex}]`));
invariant(Array.isArray(release.contributors), `${path}.contributors must be an array`);
invariant(isObject(release.links), `${path}.links must be an object`);
for (const link of ["github", "compare", "download"]) {
invariant(typeof release.links[link] === "string" && /^https:\/\//.test(release.links[link]), `${path}.links.${link} must be HTTPS`);
}
}
const sorted = [...catalog.releases].sort((a, b) => compareVersionsDesc(a.version, b.version));
invariant(
sorted.every((release, index) => release.version === catalog.releases[index].version),
"catalog.releases must be sorted newest first",
);
return catalog;
}
export async function loadCatalog(path = defaultCatalogPath) {
const catalog = JSON.parse(await readFile(path, "utf8"));
return validateCatalog(catalog);
}
export function releaseForVersion(catalog, version) {
const normalized = String(version).replace(/^(?:desktop-|npm-)?v/, "");
const release = catalog.releases.find((entry) => entry.version === normalized);
invariant(release, `release notes for v${normalized} are missing`);
return release;
}
function localized(value, lang) {
return value[lang] || value.en;
}
function refsSuffix(refs = []) {
if (!refs.length) return "";
return ` (${refs.map((ref) => `[#${ref}](https://github.com/esengine/DeepSeek-Reasonix/pull/${ref})`).join(", ")})`;
}
function renderItems(items, lang) {
return items
.map((item) => `- **${localized(item.title, lang)}** — ${localized(item.body, lang)}${refsSuffix(item.refs)}`)
.join("\n");
}
export function renderGitHubRelease(release, lang = "zh") {
const isZh = lang === "zh";
const isPreview = release.channel === "prerelease";
const channelLabel = isPreview ? (isZh ? "预览版" : "Preview") : (isZh ? "稳定版" : "Stable");
const lines = [
`> ${localized(release.summary, lang)}`,
"",
`**${isZh ? "发布渠道" : "Release channel"}:${channelLabel} · v${release.version}**`,
"",
isZh
? `[English →](https://reasonix.io/changelog/v${release.version}/?lang=en) · [网页版完整更新日志 →](https://reasonix.io/changelog/v${release.version}/)`
: `[中文 →](https://reasonix.io/changelog/v${release.version}/?lang=zh) · [Full release notes →](https://reasonix.io/changelog/v${release.version}/?lang=en)`,
"",
];
if (release.guides.length) {
lines.push(`## ${isZh ? "使用攻略" : "Guides"}`, "");
for (const guide of release.guides) {
lines.push(`- [**${localized(guide.title, lang)}**](${guide.href}) — ${localized(guide.body, lang)}`);
}
lines.push("");
}
lines.push(
`## ${isZh ? "概览" : "Overview"}`,
"",
`**Reasonix v${release.version} — ${localized(release.title, lang)}**`,
"",
localized(release.summary, lang),
"",
`${isZh ? "发布日期" : "Released"}:${release.date}`,
"",
`## ${isZh ? "重点内容" : "Highlights"}`,
"",
renderItems(release.highlights, lang),
"",
);
const headings = {
new: isZh ? "新功能" : "New",
improved: isZh ? "改进" : "Improved",
fixed: isZh ? "修复" : "Fixed",
};
for (const kind of changeKinds) {
const items = release.changes[kind];
if (!items.length) continue;
lines.push(`## ${headings[kind]}`, "", renderItems(items, lang), "");
}
lines.push(`## ${isZh ? "升级提醒" : "Upgrade notes"}`, "");
if (release.upgrade.length) lines.push(renderItems(release.upgrade, lang));
else lines.push(isZh ? "本版本无需手动迁移。" : "No manual migration is required.");
lines.push("");
lines.push(`## ${isZh ? "风险提示" : "Risk notes"}`, "");
if (release.risks.length) lines.push(renderItems(release.risks, lang));
else lines.push(isZh ? "当前没有需要额外操作的已知风险。" : "There are no known risks requiring extra action.");
lines.push("");
if (release.contributors.length) {
lines.push(
`## ${isZh ? "致谢" : "Thanks"}`,
"",
`${isZh ? "感谢本版本的贡献者" : "Thanks to the contributors in this release"}:${release.contributors
.map((name) => `[@${name}](https://github.com/${name})`)
.join("、")}`,
"",
);
}
lines.push(
`## ${isZh ? "下载与安装" : "Download and install"}`,
"",
`- [${isZh ? "官网按平台下载" : "Platform downloads"}](${release.links.download})`,
`- [${isZh ? "查看完整差异" : "Full comparison"}](${release.links.compare})`,
"",
);
return `${lines.join("\n").trim()}\n`;
}
export async function upsertRelease(release, path = defaultCatalogPath) {
const catalog = await loadCatalog(path);
const next = {
...catalog,
releases: [release, ...catalog.releases.filter((entry) => entry.version !== release.version)].sort((a, b) =>
compareVersionsDesc(a.version, b.version),
),
};
validateCatalog(next);
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
return next;
}
function parseArgs(argv) {
const [command = "validate", ...rest] = argv;
const values = { command };
for (let i = 0; i < rest.length; i += 1) {
const arg = rest[i];
if (!arg.startsWith("--")) throw new Error(`unexpected argument ${arg}`);
values[arg.slice(2)] = rest[++i];
}
return values;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const catalogPath = args.catalog ? resolve(args.catalog) : defaultCatalogPath;
const catalog = await loadCatalog(catalogPath);
if (args.command === "validate") {
console.log(`Validated ${catalog.releases.length} release note(s).`);
return;
}
if (args.command !== "render") throw new Error(`unknown command ${args.command}`);
invariant(args.version, "render requires --version");
invariant(args.output, "render requires --output");
const release = releaseForVersion(catalog, args.version);
const output = resolve(args.output);
await writeFile(output, renderGitHubRelease(release, args.lang === "en" ? "en" : "zh"));
console.log(`Rendered v${release.version} release notes to ${output}`);
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
-121
View File
@@ -1,121 +0,0 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { loadCatalog, releaseForVersion, renderGitHubRelease, validateCatalog } from "./release-notes.mjs";
import { validateReleaseEvent } from "./release-event.mjs";
test("the committed release catalog is valid and newest first", async () => {
const catalog = await loadCatalog();
assert.equal(catalog.schemaVersion, 1);
assert.equal(new Set(catalog.releases.map((release) => release.version)).size, catalog.releases.length);
});
test("tag namespaces resolve to the same product release", async () => {
const catalog = await loadCatalog();
assert.equal(releaseForVersion(catalog, "v1.17.13").version, "1.17.13");
assert.equal(releaseForVersion(catalog, "desktop-v1.17.13").version, "1.17.13");
assert.equal(releaseForVersion(catalog, "npm-v1.17.13").version, "1.17.13");
});
test("GitHub rendering keeps product sections and source PR links", async () => {
const catalog = await loadCatalog();
const markdown = renderGitHubRelease(releaseForVersion(catalog, "1.17.13"), "zh");
assert.match(markdown, /## 使用攻略/);
assert.match(markdown, /## 重点内容/);
assert.match(markdown, /## 升级提醒/);
assert.match(markdown, /## 风险提示/);
assert.match(markdown, /## 致谢/);
assert.match(markdown, /\/pull\/6460/);
assert.match(markdown, /reasonix\.io\/changelog\/v1\.17\.13/);
});
test("validation rejects bilingual drift", () => {
assert.throws(
() =>
validateCatalog({
schemaVersion: 1,
releases: [
{
version: "1.0.0",
date: "2026-01-01",
channel: "stable",
title: { en: "Title", zh: "" },
summary: { en: "Summary", zh: "摘要" },
},
],
}),
/title\.zh/,
);
});
test("managed Preview records bind every surface to one exact ordinal", () => {
const release = {
version: "1.19.0-preview.3",
releaseId: "1.19.0-preview.3",
baseVersion: "1.19.0",
date: "2026-07-31",
channel: "prerelease",
status: "reviewed",
previousRelease: "1.19.0-preview.2",
builds: {
cli: "v1.19.0-preview.3",
desktop: "v1.19.0-preview.3",
npm: "1.19.0-canary.3",
},
title: { en: "Preview", zh: "预览版" },
summary: { en: "Preview summary", zh: "预览版摘要" },
surfaces: ["cli"],
guides: [],
highlights: [{
kind: "fixed",
title: { en: "Fix", zh: "修复" },
body: { en: "A fix.", zh: "一项修复。" },
refs: [1],
}],
changes: { new: [], improved: [], fixed: [] },
upgrade: [],
risks: [],
contributors: [],
links: {
github: "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.19.0-preview.3",
compare: "https://github.com/esengine/DeepSeek-Reasonix/compare/v1.19.0-preview.2...v1.19.0-preview.3",
download: "https://reasonix.io/?download=desktop&channel=preview#start",
},
};
assert.doesNotThrow(() => validateCatalog({ schemaVersion: 1, releases: [release] }));
assert.throws(
() => validateCatalog({
schemaVersion: 1,
releases: [{ ...release, builds: { ...release.builds, npm: "1.19.0-canary.4" } }],
}),
/builds\.npm/,
);
});
test("publication marker is bound to reviewed version, channel, SHA, and builds", () => {
const release = {
version: "1.19.0-preview.3",
channel: "prerelease",
candidateSha: "a".repeat(40),
builds: {
cli: "v1.19.0-preview.3",
desktop: "v1.19.0-preview.3",
npm: "1.19.0-canary.3",
},
};
const event = {
schemaVersion: 1,
releaseId: release.version,
channel: "preview",
candidateSha: release.candidateSha,
publishedAt: "2026-07-31T00:00:00.000Z",
releaseNotesUrl: "https://reasonix.io/changelog/v1.19.0-preview.3/",
builds: release.builds,
};
assert.doesNotThrow(() => validateReleaseEvent(event, release));
assert.throws(
() => validateReleaseEvent({ ...event, candidateSha: "b".repeat(40) }, release),
/candidateSha/,
);
});
-45
View File
@@ -1,45 +0,0 @@
#!/usr/bin/env bash
# Bind a normal official release to the main-v2 commit that introduced or
# updated its reviewed release-notes record.
set -euo pipefail
if [ "$#" -ne 2 ] || [[ ! "$1" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || \
[[ ! "$2" =~ ^[0-9a-f]{40}$ ]]; then
echo "usage: validate-stable-candidate.sh MAJOR.MINOR.PATCH FULL_COMMIT_SHA" >&2
exit 2
fi
version="$1"
candidate="$2"
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
git cat-file -e "$candidate^{commit}" 2>/dev/null || {
echo "release candidate commit is unavailable: $candidate" >&2
exit 1
}
parent="$(git rev-parse "$candidate^1" 2>/dev/null)" || {
echo "release candidate must have a first parent: $candidate" >&2
exit 1
}
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/reasonix-stable-candidate.XXXXXX")"
cleanup() {
case "$work_dir" in
*/reasonix-stable-candidate.*) rm -rf -- "$work_dir" ;;
*) echo "refusing to clean unexpected candidate directory: $work_dir" >&2 ;;
esac
}
trap cleanup EXIT
git show "$candidate:release-notes/releases.json" >"$work_dir/current.json" || {
echo "release candidate does not contain release-notes/releases.json: $candidate" >&2
exit 1
}
git show "$parent:release-notes/releases.json" >"$work_dir/previous.json" || {
echo "release candidate parent does not contain release-notes/releases.json: $parent" >&2
exit 1
}
node "$script_dir/validate-stable-release-record.mjs" \
"$work_dir/current.json" "$version" "$work_dir/previous.json"
echo "Stable candidate is bound to its reviewed Notes change: v$version at $candidate"
@@ -1,26 +0,0 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { isDeepStrictEqual } from "node:util";
import { releaseForVersion, validateCatalog } from "./release-notes.mjs";
const [catalogPath, version, previousCatalogPath] = process.argv.slice(2);
if (!catalogPath || !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version || "")) {
throw new Error("usage: validate-stable-release-record.mjs CATALOG MAJOR.MINOR.PATCH [PREVIOUS_CATALOG]");
}
const catalog = validateCatalog(JSON.parse(readFileSync(catalogPath, "utf8")));
const release = releaseForVersion(catalog, version);
if (release.version !== version || release.channel !== "stable" || release.status !== "reviewed") {
throw new Error(`v${version} must have one reviewed Stable release record`);
}
if (previousCatalogPath) {
const previousCatalog = validateCatalog(JSON.parse(readFileSync(previousCatalogPath, "utf8")));
const previousRelease = previousCatalog.releases.find((entry) => entry.version === version);
if (previousRelease && isDeepStrictEqual(previousRelease, release)) {
throw new Error(`v${version} reviewed Stable record was not introduced or changed by the candidate commit`);
}
}
console.log(`reviewed Stable release record verified: v${version}`);
@@ -1,56 +0,0 @@
# Starter Extension
该目录是一个完整、可安装的 Extension Protocol v2 插件。它的 Sidecar
拦截 `input.receive`;以 `starter: ` 开头的文本会在送入模型前追加
` [rewritten by starter-extension]`。
二进制统一使用 `.exe` 后缀是有意设计:这样所有平台可以共用同一份
Manifest。Unix 可以正常执行该文件,Windows 则需要可执行文件后缀。
## 构建与安装
在 macOS 或 Linux 中进入本目录后运行:
```sh
go build -o bin/starter-extension.exe .
plugin_root="$(pwd -P)"
reasonix plugin install "$plugin_root" --dry-run
reasonix plugin install "$plugin_root" --link --replace --yes
```
Windows PowerShell:
```powershell
go build -o bin/starter-extension.exe .
$pluginRoot = (Resolve-Path .).Path
reasonix plugin install $pluginRoot --dry-run
reasonix plugin install $pluginRoot --link --replace --yes
```
安装前请检查 dry-run 输出中的 `FULL TRUST` 区块。链接安装会持续信任本目录
后续的内容变化,而且代码型 Runtime 运行在 Reasonix Sandbox 之外。
启动新会话,或在当前会话空闲时运行 `/reload`,然后发送:
```text
starter: 解释 Extension Protocol Sidecar 的作用
```
模型会收到改写后的文本。修改 `main.go`、重新构建、执行 `/reload` 后即可
再次验证。Manifest 或二进制校验失败时,运行
`reasonix plugin doctor starter-extension`。
## 下一步
- [`../../README.md`](../../README.md) 说明 SDK 回调与并发契约。
- [`../../../../docs/EXTENSIONS.zh-CN.md`](../../../../docs/EXTENSIONS.zh-CN.md)
说明重载、性能、缓存、兼容性与信任模型。
- [`../../../../docs/PLUGIN_PACKAGES.zh-CN.md`](../../../../docs/PLUGIN_PACKAGES.zh-CN.md)
定义 Manifest v2 的全部字段。
- [`../../../../docs/EXTENSION_PROTOCOL.zh-CN.md`](../../../../docs/EXTENSION_PROTOCOL.zh-CN.md)
是线上协议参考。
- [`../fullsidecar/main.go`](../fullsidecar/main.go) 展示 Provider、结构化 UI、
strategy、工具、content ref 与关闭流程。
要发布可分发插件,请为目标平台构建二进制,确保 Manifest runtime 路径与
包内文件一致,并提供不可变的源码或 release artifact 供用户安装前审查。
-203
View File
@@ -1,203 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
const (
specEnglish = "docs/SPEC.md"
specChinese = "docs/SPEC.zh-CN.md"
)
// lockedSpecSections are the sections both SPECs carry in full. The Chinese
// companion abridges the rest on purpose, so parity is declared here rather
// than inferred from the tree — the same reason the host's sensitive paths
// are. Adding a section commits every later change to it to both files.
var lockedSpecSections = []string{"3.11", "3.13", "3.14", "3.15", "3.16"}
var specHeadingRe = regexp.MustCompile(`^#{2,4}\s+(\d+(?:\.\d+)*)\s`)
// specBlock is one blank-line-separated run, or one fenced region. Size is
// carried only where a translation cannot legitimately change it: a table row
// and a fenced line survive translation, a wrapped sentence does not.
type specBlock struct {
kind string
lines int
}
type specSection struct {
line int
blocks []specBlock
}
// checkSpecParity holds the two SPECs to the same structure wherever they claim
// to say the same thing. It reads headings and block shape only — comparing
// wording would fire on every legitimate translation choice, and is a judgement
// this tool cannot read from structure.
func checkSpecParity(root string) []Finding {
en, enOrder, err := readSpecSections(filepath.Join(root, specEnglish))
if err != nil {
return nil
}
zh, zhOrder, err := readSpecSections(filepath.Join(root, specChinese))
if err != nil {
return nil
}
return specParityFindings(lockedSpecSections, en, enOrder, zh, zhOrder)
}
func specParityFindings(locked []string, en map[string]specSection, enOrder []string, zh map[string]specSection, zhOrder []string) []Finding {
var out []Finding
if msg := specHeadingDiff(enOrder, zhOrder); msg != "" {
out = append(out, Finding{specChinese, 1, ruleSpecParity, msg, 1})
}
for _, num := range locked {
src, srcOK := en[num]
dst, dstOK := zh[num]
switch {
case !srcOK:
out = append(out, Finding{specEnglish, 1, ruleSpecParity, specMissing(num), 1})
case !dstOK:
out = append(out, Finding{specChinese, 1, ruleSpecParity, specMissing(num), 1})
default:
if msg := specShapeDiff(num, src.blocks, dst.blocks); msg != "" {
out = append(out, Finding{specChinese, dst.line, ruleSpecParity, msg, 1})
}
}
}
return out
}
func specMissing(num string) string {
return fmt.Sprintf("§%s is locked for translation parity but is missing here", num)
}
func specHeadingDiff(en, zh []string) string {
for i := range max(len(en), len(zh)) {
switch {
case i >= len(zh):
return fmt.Sprintf("§%s has a heading in %s and none here", en[i], specEnglish)
case i >= len(en):
return fmt.Sprintf("§%s has a heading here and none in %s", zh[i], specEnglish)
case en[i] != zh[i]:
return fmt.Sprintf("heading %d is §%s here and §%s in %s", i+1, zh[i], en[i], specEnglish)
}
}
return ""
}
func specShapeDiff(num string, en, zh []specBlock) string {
if len(en) != len(zh) {
return fmt.Sprintf("§%s is locked for translation parity: %d blocks here, %d in %s", num, len(zh), len(en), specEnglish)
}
for i := range en {
if en[i].kind != zh[i].kind {
return fmt.Sprintf("§%s block %d is %s here and %s in %s", num, i+1, zh[i].kind, en[i].kind, specEnglish)
}
if en[i].lines != zh[i].lines {
return fmt.Sprintf("§%s %s block %d has %d lines here and %d in %s", num, zh[i].kind, i+1, zh[i].lines, en[i].lines, specEnglish)
}
}
return ""
}
func readSpecSections(path string) (map[string]specSection, []string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, nil, err
}
sections, order := parseSpecSections(data)
return sections, order, nil
}
func parseSpecSections(data []byte) (map[string]specSection, []string) {
sections := map[string]specSection{}
var order []string
var num string
var body []string
line := 0
flush := func() {
if num != "" {
sections[num] = specSection{line: line, blocks: specBlocks(body)}
}
}
for i, raw := range splitLines(data) {
m := specHeadingRe.FindStringSubmatch(raw)
if m == nil {
if num != "" {
body = append(body, raw)
}
continue
}
flush()
num, line, body = m[1], i+1, nil
order = append(order, num)
}
flush()
return sections, order
}
func specBlocks(body []string) []specBlock {
var out []specBlock
var cur []string
fenced := false
flush := func() {
if len(cur) > 0 {
out = append(out, classifySpecBlock(cur))
cur = nil
}
}
for _, raw := range body {
fence := strings.HasPrefix(raw, "```")
switch {
case fence && !fenced:
flush()
cur = append(cur, raw)
fenced = true
case fence:
cur = append(cur, raw)
flush()
fenced = false
case fenced:
cur = append(cur, raw)
case strings.TrimSpace(raw) == "":
flush()
default:
cur = append(cur, raw)
}
}
flush()
return out
}
func classifySpecBlock(lines []string) specBlock {
switch {
case strings.HasPrefix(lines[0], "```"):
return specBlock{kind: "code", lines: len(lines)}
case strings.HasPrefix(lines[0], "|"):
return specBlock{kind: "table", lines: len(lines)}
case specIsList(lines):
return specBlock{kind: "list"}
}
return specBlock{kind: "prose"}
}
// specIsList reads a continuation line by its indent, which is what separates a
// wrapped bullet from the paragraph that follows a list.
func specIsList(lines []string) bool {
for _, raw := range lines {
if strings.HasPrefix(raw, " ") {
continue
}
trimmed := strings.TrimSpace(raw)
if strings.HasPrefix(trimmed, "-") || strings.HasPrefix(trimmed, "*") || strings.HasPrefix(trimmed, "1.") {
continue
}
return false
}
return true
}
-107
View File
@@ -1,107 +0,0 @@
package main
import (
"strings"
"testing"
)
// The two drifts this rule exists to catch, reduced to the smallest documents
// that carry them: a paragraph that never crossed, and a table row that did not.
const parityEnglish = `### 3.14 Fleet is a small dependency graph
A fleet item may declare ` + "`id`" + ` and ` + "`depends_on`" + `.
| Given to the child | Where it comes from |
| --- | --- |
| The task text | the user turn itself |
| A dependency's final answer | ` + "`<upstream-results>`" + ` |
The edge carries the dependency's answer, not only its order.
`
// findingsFor locks §3.14 alone, so a fixture may carry the one section a case
// is about rather than every section the real rule locks.
func findingsFor(en, zh string) []Finding {
enSections, enOrder := parseSpecSections([]byte(en))
zhSections, zhOrder := parseSpecSections([]byte(zh))
return specParityFindings([]string{"3.14"}, enSections, enOrder, zhSections, zhOrder)
}
func TestSpecParityAcceptsAMatchingTranslation(t *testing.T) {
zh := strings.NewReplacer(
"Fleet is a small dependency graph", "fleet 是一张小依赖图",
"A fleet item may declare", "fleet item 可以声明",
"Given to the child", "交给子智能体的",
"Where it comes from", "来源",
"The task text", "任务文本",
"the user turn itself", "user turn 本身",
"A dependency's final answer", "依赖的最终答案",
"The edge carries the dependency's answer, not only its order.",
"边携带的是依赖的答案,不只是它的次序。",
).Replace(parityEnglish)
if got := findingsFor(parityEnglish, zh); len(got) != 0 {
t.Fatalf("a faithful translation must produce no findings, got %+v", got)
}
}
func TestSpecParityCatchesAParagraphThatNeverCrossed(t *testing.T) {
zh := strings.Replace(parityEnglish,
"\nThe edge carries the dependency's answer, not only its order.\n", "\n", 1)
got := findingsFor(parityEnglish, zh)
if len(got) != 1 {
t.Fatalf("findings = %+v, want exactly one", got)
}
if !strings.Contains(got[0].Msg, "2 blocks here, 3 in") {
t.Fatalf("message must name the block counts, got %q", got[0].Msg)
}
if got[0].Rule != ruleSpecParity || got[0].File != specChinese {
t.Fatalf("finding = %+v, want the %s rule on %s", got[0], ruleSpecParity, specChinese)
}
}
func TestSpecParityCatchesAMissingTableRow(t *testing.T) {
zh := strings.Replace(parityEnglish,
"| A dependency's final answer | `<upstream-results>` |\n", "", 1)
got := findingsFor(parityEnglish, zh)
if len(got) != 1 {
t.Fatalf("findings = %+v, want exactly one", got)
}
if !strings.Contains(got[0].Msg, "table block 2 has 3 lines here and 4") {
t.Fatalf("message must name the missing row, got %q", got[0].Msg)
}
}
func TestSpecParityCatchesASectionOnlyOneSideHas(t *testing.T) {
got := findingsFor(parityEnglish, "")
if len(got) != 2 {
t.Fatalf("findings = %+v, want a heading-sequence finding and a missing-section one", got)
}
if !strings.Contains(got[0].Msg, "§3.14 has a heading in") {
t.Fatalf("heading finding = %q", got[0].Msg)
}
if !strings.Contains(got[1].Msg, "§3.14 is locked for translation parity but is missing here") {
t.Fatalf("missing-section finding = %q", got[1].Msg)
}
}
// TestSpecParityIgnoresProseWrapping is the false-positive guard: English wraps
// at 80 columns and Chinese does not, so line counts inside a paragraph must
// never be compared.
func TestSpecParityIgnoresProseWrapping(t *testing.T) {
en := "### 3.14 x\n\nOne sentence split\nacross three\nsource lines.\n"
zh := "### 3.14 x\n\n一句话写在一行里。\n"
if got := findingsFor(en, zh); len(got) != 0 {
t.Fatalf("prose wrapping must not be a finding, got %+v", got)
}
}
// TestSpecParityHoldsTheRealTree pins the invariant the rule was written for:
// every locked section is in parity right now, so any later drift is new.
func TestSpecParityHoldsTheRealTree(t *testing.T) {
if got := checkSpecParity("../.."); len(got) != 0 {
for _, f := range got {
t.Errorf("%s:%d: %s", f.File, f.Line, f.Msg)
}
t.Fatalf("the two SPECs have drifted in %d locked place(s)", len(got))
}
}