Skip to content

docs: adopt four-bucket docs layout and progressive AGENTS index - #2374

Closed
azi44eo wants to merge 3345 commits into
GCWing:mainfrom
azi44eo:docs-0818
Closed

azi44eo wants to merge 3345 commits into
GCWing:mainfrom
azi44eo:docs-0818

Conversation

@azi44eo

@azi44eo azi44eo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Align the code repository around four authoritative documentation buckets — architecture/, guideline/, specs/, and plans/ — and turn the root AGENTS files into progressive-disclosure indexes.

Summary

  • Add complete indexes for the four authoritative documentation buckets and route root AGENTS guidance through standards, architecture, cross-cutting, and task-based maps.
  • Move stable architecture, repository operations, feature specs, implementation plans, and SDLC governance material to their owning buckets.
  • Add documentation governance, command, verification, logging, host/remote, agent-loop, theme, and Spec/Plan template guidance.
  • Keep code-coupled Feishu setup and release-download verification guides in this repository.
  • Preserve public paths used by released clients and durable links with compatibility pointer pages; canonical content has one owner.
  • Preserve the existing performance audits and SDLC external research as dated, non-normative references instead of deleting them.
  • Preserve existing normative meaning, including the absolute tokio/full prohibition and owner-scoped Rust verification.

Type and Areas

Type: documentation / repository governance

Areas: root AGENTS and contributor entrypoints, docs/, Web UI documentation URLs, appearance audit documentation paths

Motivation / Impact

The old layout mixed stable architecture, operational rules, feature designs, plans, product setup guides, research, and temporary-process paths. The new layout gives each durable document one canonical owner while keeping high-frequency rules discoverable within two hops from AGENTS.md.

There is no product-logic change. User-visible impact is limited to documentation/help links, and legacy public paths remain reachable for already released clients.

Migration Map

Old location Canonical location / disposition
docs/development/{i18n,releasing,ui-testids*} docs/guideline/
docs/remote-connect/feishu-bot-setup* docs/guideline/feishu-bot-setup*; old paths retained as compatibility pointers
docs/verify-downloads* docs/guideline/verify-downloads*; old paths retained as compatibility pointers
docs/features/{agent-hooks*,remote-workspaces,session-runtime-usage-report-design} docs/specs/; old public paths retained as compatibility pointers
docs/superpowers/specs/* docs/specs/*
docs/superpowers/plans/* docs/plans/*-plan.md
docs/sdlc-harness/architecture/* and design.md docs/architecture/* and docs/architecture/sdlc-governance-architecture.md
docs/sdlc-harness/features/*, product requirements, metrics, and traceability docs/specs/*
docs/sdlc-harness/{implementation-plan,agent-workflow-staged-plan}.md docs/plans/sdlc-governance-*.md
docs/sdlc-harness/governance/self-governance-notes.md docs/guideline/self-governance-notes.md
docs/sdlc-harness/research/external-research.md docs/specs/sdlc-governance-external-research.md as a dated non-normative reference
docs/performance/01-compile-performance.md docs/plans/2026-08-14-compile-and-dependency-governance-plan.md
docs/performance/02-04 reviews Dated reference documents under docs/specs/; implementation requires revalidation
docs/sdlc-harness/README.md Navigation and stable boundaries folded into docs/architecture/sdlc-governance-architecture.md
Empty docs/specs/templates/plan.md introduced by the initial migration Removed; the canonical plan template is docs/plans/templates/plan.md

Compatibility / Reviewer Notes

  • The final PR has no dependency on an external bitfun_doc repository.
  • Compatibility pages contain no duplicated authority; they only point to canonical pages and are indexed in docs/README.md.
  • Verified on the PR head that legacy Feishu, download-verification, and Agent Hooks URLs return HTTP 200.
  • No remote runtime scenario changed. The only product path touched is a documentation URL, with the released-client path preserved.

Verification

  • git diff --check
  • pnpm run check:repo-hygiene
  • pnpm --dir src/web-ui run gen:types — 34 tests passed
  • pnpm run type-check:web
  • pnpm run appearance:contract-audit — passed; 16 existing advisory warnings
  • Scripted local Markdown path audit — no new missing local links versus the base snapshot
  • Canonical directory/index inventory audit — 0 unindexed pages
  • Manual old-to-new mapping review against docs/README.md and docs/guideline/docs-governance.md

Checklist

  • This PR is focused and does not include secrets, temporary prompts, generated scratch files, or unrelated artifacts.
  • Relevant verification is recorded above, or skipped checks are explained.
  • User-facing strings, docs, and locales are updated where applicable.

bobleer and others added 30 commits August 10, 2026 22:16
Remote scenarios routinely put two different BitFun versions on one
connection, and users upgrade in place, but the agent guide had no rule
covering either. Add an "Upgrade compatibility" global rule next to the
remote scenarios section, matching what the existing remote-workspace and
transport designs already practice:

- persisted shapes stay tolerant, defaulted, and never repurposed;
- unparseable data degrades instead of being deleted or reset;
- cross-version boundaries advertise and check a capability rather than
  assuming behavior from a package version;
- a rename is a migration, including the data it references;
- upgrade coverage means legacy deserialization and old-payload round
  trips, not only the current shape.

AGENTS-CN.md is updated to match.
The Web UI transport adapter denies 10 controller-owned commands that
neither peer host refuses: the nine `speech_*` capture/model commands and
`dispatch_continue`. The controller-side list is only an optimization —
an older controller build, or any non-Web-UI controller, still reaches a
peer host over HostInvoke, and the host would then run them.

That means microphone capture sessions and speech model file management
executing on the peer instead of the machine the user speaks at, and a
dispatch control-plane verb running against the peer's outbound observer
records and SSH credentials rather than the controller's. Every sibling
dispatch verb is already denied on all three lists; `dispatch_continue`
was simply missed.

Deny both families on the desktop and CLI peer hosts, and add contract
tests covering them.

The README already required these three lists to stay aligned, but
nothing enforced it. Add a peer command policy check to the core boundary
checker that fails when a controller-denied command is missing from either
host list, so the boundary cannot drift again. The check is one-way: a
host denying more than the controller stays allowed. The CLI's four
pre-handled control-plane commands are declared exceptions, and a stale
exception is itself reported.

Verified: cargo test -p bitfun-cli --bins peer_host, cargo test -p
bitfun-desktop --lib peer_host_invoke / remote_workspace_policy, and the
new check both passing on the fix and reporting all 10 commands without it.
Reconnecting to a peer device left the chat blank until the user clicked a
session in the sidebar. Three gaps in `initializeWorkspace`, all reachable
only in Peer Device Mode because `clearAllSessionsForPeerSwitch` is the one
place that bumps the store surface generation:

1. A metadata page loaded across a surface-generation bump is discarded by
   `processPersistedSessionMetadataList`, but the page is still returned
   with its sessions. The caller then sees "history exists" over an empty
   store. Reload once against the settled generation.

2. History was only restored on the auto-select path, so a session that was
   already active but still metadata-only was never hydrated. The breadcrumb
   and turn rail render from the catalog while the message area stays empty
   — the reported symptom. Restore history for an active historical session
   too.

3. When metadata claimed sessions but none were selectable, initialize
   returned true while selecting nothing. The caller reads true as "do not
   create a session", so the surface ended up with neither. Report no
   history so the caller creates against the live workspace instead.

No persisted shape, command, or protocol changes; an older peer host is
unaffected.

Verified: all three tests fail without the fix and pass with it; the 8
existing FlowChatManager cases are unchanged; 252 tests across
src/flow_chat/services pass; pnpm run type-check:web clean.
…r-retries

fix(agent): unify live model retry budget
`browser.wait` read only `duration_ms`, so the very plausible
`{ "ms": 1800000 }` was dropped and the call fell through to a branch that
returned `{ success: true }` instantly. An agent asked to pause 30 minutes
got "Wait completed" back in milliseconds and moved straight on. The
action documented no parameters at all, so the model had to guess the key,
and even a correct guess was silently capped at 30 seconds.

- Accept the spellings models emit: `duration_ms` / `ms` / `wait_ms` /
  `sleep_ms`, plus `seconds` / `secs` variants, numeric strings included.
- Reject a `wait` carrying neither duration nor condition with
  INVALID_PARAMS instead of reporting a success that never waited.
- Raise the cap to 60 minutes, in step with AgentWait's MAX_TIMEOUT_MS, and
  report `ms` / `requested_ms` / `clamped` so a shortened wait says so.
- Race the sleep against the turn's cancellation token, and stop
  `call_impl` from folding Cancelled into an `ok: false` envelope — a stop
  during a long pause must not wait out the pause, nor look like a tool
  error the model tries to recover from.
- Serve duration waits before session resolution: a pure pause touches no
  page, and agents pace themselves long before they open a browser.
- Resolve `{ condition, timeout_ms }`: the condition always wins and any
  duration bounds it, rather than sleeping and never looking at the page.
  Condition waits keep their previous 15s default, now configurable.
- Document all of it in the tool description, and point repeating schedules
  at the Cron tool, which ends the turn instead of pinning it open.
Six things the session screen got wrong on device, all reported together.
They land in one commit because they overlap in the same few files.

The timeline never followed streaming output. `scrollEdge(Edge.Bottom)` only
existed inside the floating button's `onClick`, and the list was explicitly
`stackFromEnd(false)`, so a bubble growing every 40ms grew below the viewport.
Flipping `stackFromEnd` handles the growth case; a `timelineRevision` monitor
covers whole-message inserts, and a `stickToBottom` flag reads `isAtEnd()` so
scrolling back through history is not fought by the follow.

The composer locked to Stop for the whole run. `isStopping` folded dictation
and a running turn into one flag, but they sit on opposite sides of the draft:
dictation outranks it because the draft is still being spoken, a running turn
only outranks it where the draft has nowhere to go. Split into
`isVoiceListening` and `isTurnRunning`; remote sessions can hand a message over
mid-run, so a draft now wins there and the relay queues it.

Stop did not stop. Between sending a message and the server's turn_id arriving,
`remoteActiveTurnId()` sliced the local `active-pending-<id>` into
`pending-<id>` and sent it as a turn_id. The server read that as
`StaleRequestedTurn` and kept running. It now returns '' in that window, which
the server resolves as CancelCurrent. The failure was also invisible — it went
to `statusText`, which only renders while disconnected — so the stop path now
reports through the existing toast channel.

AskUserQuestion options were cut off. `OptionRow` was pinned to 36px with no
`maxLines` on the label and no `layoutWeight` on the description, so a long
option lost its tail with nothing to indicate it. The row now grows with its
content and label/description stack instead of competing for one line.

Picked images rendered outside the input. They now sit in a strip inside the
composer card, left-aligned — `Scroll` centres content narrower than its
viewport, which parked a lone thumbnail mid-composer.

The plus button opened an attachment panel offering quick prompts and a single
image entry. A message can only carry images (`RemoteCommand::SendMessage` has
no file channel), so a menu there was a menu of one; plus now opens the album
directly, matching local chat. The panel and its orphaned strings are gone.
Image encoding also got a size budget, since the transport doubles what it is
handed: base64 into the command JSON, then base64 again after AES-GCM.
`is_valid_encrypted_payload` caps the ciphertext at 48 MB, but the route
inherited Axum's 2 MB `DefaultBodyLimit`, which rejects the body before the
handler — including its auth check — ever runs. A phone attaching a photo got a
bare 413 from a route that claimed to accept 24x that.

The limit is derived from `MAX_ENCRYPTED_PAYLOAD_BYTES` so the two cannot drift
apart again, with slack for the JSON envelope and the base64 nonce. Two tests
pin both ends: a 3 MB body reaches the handler, and one past the route ceiling
is still refused.
Asked to sweep a set of channels every 30 minutes, a Cowork-mode agent
replied that it had no cron tool and fell back to chaining long `wait`
calls. It was telling the truth: `Cron` was in Claw's tool list and
nowhere else, so in Cowork, Team, DeepResearch, and the four shared
coding modes the agent could not see it at all. Deferred exposure is not
the cause — deferred tools are advertised by name — the tool simply was
not in those modes' `default_tools`.

ControlHub's `wait` documentation now tells the agent to schedule
repeating work with Cron instead of holding the turn open, and ControlHub
ships in all of those modes, so the guidance pointed at a tool the agent
did not have.

- Add `Cron` to Cowork, Team, DeepResearch, and `shared_coding_mode_tools`
  (agentic / debug / multitask / plan), so it is available everywhere
  ControlHub is.
- Cover the pairing with an invariant test over the built-in modes; it
  caught the shared coding baseline, which the first pass had missed.
- Say in ControlHub's description what to do if Cron is genuinely absent,
  rather than leaving a chain of long waits as the silent fallback.

Scheduling stays behind the normal permission gate — Cron is not
read-only and emits a `custom_tool` intent — so this widens what the
agent can propose, not what it can do unattended.
Asked to pace itself with a cron job, the agent created one and then kept
working for another half hour before stopping on its own. Nothing was
broken: `Cron add` returns "Created scheduled job ..." and nothing more, so
the call reads as an ordinary success, and no tool can end a turn — only
the model stopping ends it. With the loop it just handed to the scheduler
still in its head, it drives that loop itself.

That is worse than untidy. A scheduled run is submitted as a queued dialog
turn at low priority, so it is never run in parallel with the turn already
in flight; a round that outlives the interval simply delays the trigger it
is racing. The round in question took 32m against a 30m interval, which
turns "every 30 minutes" into back-to-back rounds.

- Say it in the `add` result, but only when the job actually takes over
  this turn's cadence: it repeats, and it targets this session. A one-shot
  reminder, or a job scheduled for some other session, must not cut the
  current turn short.
- Say it in the tool description too, so the model knows before it commits
  to a plan rather than after — including that a run firing into a busy
  session is queued, so the interval wants to be longer than a round.
- Drop the claim, added with the browser.wait fix, that Cron "ends the turn
  and re-invokes you". It does not, and a model that believed it would keep
  the turn open waiting for an end that never comes.
…r path

A single Feishu "send one message" run took 73 tool round-trips. Tracing
the log back, most of them came from four defects that compound.

**`describe_screen` was blind on macOS.** `macos_foreground_application`
built its AppleScript with a `try … end try` *block* in expression
position, which AppleScript rejects at compile time (-2741). The command
always exited non-zero, so the function always returned `None` — and
`describe_screen` derives its target app from that value. Every call
reported `foreground_application: null` and `ax_tree_text: null`. The
agent reasonably concluded its own tool output was being truncated,
stopped trusting results, and fell back to `screencapture` +
image-analysis as a substitute for eyes: 10 extra model round-trips that
returned prose instead of coordinates and misread the screen repeatedly.

Replaced with an in-process `NSWorkspace.frontmostApplication` read, and
did the same for `macos_ax_ui::frontmost_pid`, which shelled out to
`osascript` on every call — three spawns per `describe_screen`, each
~120ms, each able to block on a System Events AppleEvent timeout.

**Enter was permanently refused in text-only mode.** The stale-capture
guard is cleared only by a successful capture, but a text-only
`screenshot` short-circuits before capturing. So the guard latched: its
own error said "call `screenshot` first", calling it changed nothing, and
every `click`/Enter stayed blocked. The observed escape was the agent
bypassing the tool with raw `osascript … keystroke return`, which skips
every check the guard exists to enforce. `describe_screen` (the text-only
equivalent of looking) and the text-only `screenshot` stub now waive it.
`paste` also clears it unconditionally instead of only when `submit:true`
— paste-then-Enter is the common shape and the pointer never moved.

**Results were mostly duplicate.** Every `app_state` carried both
`tree_text` and `app_state_nodes`, the same nodes re-serialised as
verbose JSON. `render_tree_text` already emits every addressable field,
nothing consumed the array, and it was 82 KB of a 107 KB result. Dropped,
`node_count` kept.

**`get_app_state` returned mostly closed menus.** Observing a windowless
app produced 188 nodes, 180 of them menu items at zero-size off-screen
frames — unclickable until the menu opens. Closed `AXMenu` subtrees are
no longer walked; the container stays visible and a note points at
`get_app_shortcuts`.

Also: `open_app` reported `success: true` for an app running with no
window, which is how this run lost ~15 calls rediscovering that
`activate` does not reopen an Electron window. It now resolves the bundle
id (the launch name, executable name and bundle id are routinely three
different strings), polls for a window, retries via `open -b`, and
reports `window_count` / `windowless`.

Tests: AppleScript templates are now compile-checked with `osacompile`,
which catches exactly the class of bug above without executing anything.

Drive-by: `embedded_relay_host` tests reserved an ephemeral port, dropped
the listener, then assumed it was still free. Harmless on an idle machine
and ~80% failing once the suite spawns subprocesses. Port acquisition and
the release assertions now retry.
The macOS branch gained bundle_id / process_name / window_count /
launch_path; the other two construct the same struct and would not
compile without them.

Both leave the identity fields None rather than guessing: neither
`start` nor `xdg-open` reports what it launched, so there is no pid to
resolve identity or a window count from. `window_count: Some(0)` would
tell the model the app is definitely windowless when it was simply
never measured.
…names

Only "Safari" was covered, which never exercises applescript_quote.
Compiling the escaped forms of a quote, a backslash and CJK is what
proves the escaping matches AppleScript's actual string-literal syntax
rather than a plausible guess about it.
The two lines this prompt was missing are the ones that would have ended
the observed failure early. When describe_screen returned nulls the agent
concluded its own output was truncated and improvised screencapture plus
image analysis as a substitute for eyes.

State plainly that an empty ax_tree_text is a result with a reason
attached (ax_tree_status / ax_tree_note), that re-calling returns the
same thing, and that building eyes out of screencapture costs a model
round-trip per glance and returns prose instead of coordinates.
…tron apps

Fixing `foreground_application` gave `describe_screen` an app to look at.
It was still walking only 8 levels into the focused window, which is fine
for a native Cocoa app and far too shallow for the Electron / WebView
clients agents are most often asked to drive.

Measured against a real Electron window (focused window only):

    depth  8:    17 nodes,    7 actionable,   1 KB
    depth 12:    25 nodes,   15 actionable,   2 KB
    depth 16:    50 nodes,   40 actionable,   5 KB
    depth 20:   207 nodes,  197 actionable,  27 KB
    depth 24:   233 nodes,  223 actionable,  31 KB
    depth 32:  1289 nodes, 1279 actionable, 206 KB

Seven actionable elements is not enough to find a search field or a send
button, so the tree read as "this app has no AX tree" and pushed the
agent onto OCR and screenshot guessing. The actionable layer appears
around 20; past it the payload grows far faster than the number of things
worth clicking.

Depth is a poor proxy for size, though — a document or a long list can
multiply that 27 KB — so the returned tree is also capped at 60 KB, cut on
a line boundary. The clip announces itself and says a control that is
missing from the view may still exist: an agent that reads a truncated
tree as the whole UI concludes the control is not there and gives up.

The `#[ignore]`d dump test now prints this depth profile, so the constant
can be retuned against evidence rather than intuition.
…e index

The 60 KB cap I added in the previous commit sliced the tree at a byte
offset. `&str[..n]` panics when `n` falls inside a multi-byte character,
so a CJK app tree — the kind most likely to be large enough to hit the
cap in the first place — would panic the whole tool call roughly two
times in three.

Walk back to a char boundary before slicing.

The first tests I wrote for this passed against the broken version:
repeating a fixed line, and repeating a bare 3-byte character, both
happen to land exactly on 60_000. Shifting the content by one and two
bytes is what exposes it, so the test now covers all three alignments and
was confirmed to fail without the fix.
…ap osascript

Four follow-ups left open by GCWing#2224, each a case where a result told the
agent something that was either far too large or quietly untrue.

**`get_app_state` was unbounded.** `describe_screen` got a 60 KB cap; the
explicit query did not. Measured unbounded output on real Electron apps
was 220-390 KB from a single call — roughly 100k tokens spent on one look
at one app. Capped at 120 KB (higher, because asking for an app's tree is
an explicit request, but still a ceiling), reusing the same clip that
lands on a char boundary and announces itself. Applied in
`snap_state_json`, so `app_click` / `app_type_text` / `app_scroll` /
`app_key_chord` / `app_wait_for` are covered too — they all carry the
same post-action tree and shared the same risk.

**`analyze_image` reported the resized dimensions as the file's.** Large
screenshots get downscaled to fit the provider (repeated 0.75x passes,
floor 64px), and `ProcessedImage` kept only the final size — the source
dimensions were computed and dropped. So a caller mapping anything the
vision model said back to the screen was off by an unknown factor, with
nothing in the result hinting at it. `ProcessedImage` now carries
`original_width` / `original_height` with `scale()` and `was_resized()`;
`analyze_image` and `view_image` report both frames.

`analyze_image` also states plainly what its numbers are: any position in
the prose is a vision model's estimate in the resized frame, not a
measurement and not a click target — use `locate` / `move_to_text` /
`describe_screen`, which return real coordinates. Building a coordinate
contract on estimated numbers would be worse than saying they are
estimates.

**`open_app` could block for two minutes.** `activate` sends an AppleEvent
and waits for the app to answer; a hung app does not, and macOS's default
AppleEvent timeout is 120s, held on a blocking thread with the agent
unaware. `Command::output()` has no timeout, so osascript now runs under
a polled 10s deadline and is killed past it. A timeout reports as a
failed launch the agent can act on, not an opaque io error.

**`interaction_state.displays` rode on every result.** On a single-screen
machine it repeats what `active_display_id` already says. Sent only when
more than one display is attached; `list_displays` and `describe_screen`
still report the full list on demand.
Keep Core product-full explicit, split ACP client/server roles, and guard every product consumer against capability-union regressions.

BREAKING CHANGE: bitfun-core default features are now empty; consumers that require the full compatibility surface must enable product-full or select explicit owner features.
Configuring skills for Computer Use failed with "Skill 切换失败". The
backend was rejecting the write: `persist_agent_profile_from_value`
resolved default tools through `get_mode_defaults()`, which lists only
`AgentCategory::Mode` agents, so any sub-agent id fell through to
"Agent does not exist".

`ComputerUse` is exactly that case — registered as a built-in sub-agent
(agent-runtime `agents.rs`, with a contract test pinning it), yet
rendered by the agents scene as a core agent card, and the only built-in
sub-agent shipping the `Skill` tool. Reads never validated the agent, so
the picker listed skills and accepted toggles that could never be saved.
Every write path was affected: `replace_mode_skill_selection`,
`set_mode_skill_disabled` and `reset_mode_skill_selection` all funnel
through the same persist call.

The mode-only lookup was correct when it was written, when the function
was `persist_mode_config_from_value` and its error read "Mode does not
exist". Renaming it to the agent level widened what it promised without
widening what it resolved; the agents scene later opened skill editing to
sub-agents, which made the gap reachable.

- Resolve default tools from modes and sub-agents alike.
- Widen profile defaults so canonicalization recognizes sub-agent
  profiles and normalizes them like any other.
- Keep unrecognized profiles instead of pruning them. Canonicalization
  runs at startup with no workspace, so project-scoped sub-agents are
  invisible there and would lose their stored selection on every launch
  now that it can be written at all. Records that no longer deserialize
  are still dropped, since `get_agent_profile_configs` reads the map as a
  whole and one bad entry would silently empty it.

The runtime side already worked for sub-agents: skill filtering keys on
an opaque `mode_id` and shares `resolve_skill_state_for_mode` with the
UI listing, so a saved selection takes effect without further changes.

The new test pins the invariant rather than the instance: any agent
whose default tools include `Skill` must resolve to a profile default.
Scenes stay mounted while the workbench shows another one, so a paused
thread goal opened its resume modal from the background: the dialog
landed on top of the MiniApp gallery, settings, or whatever scene the
user had just clicked into.

Gate the prompt on the session scene being the one in front, and treat a
change of visit -- leaving the session or opening another one -- as a
retract plus re-arm, so the prompt returns when the user opens the paused
session again instead of chasing them across the app.
The pending-queue "send now" button refused messages with attachments or
structured metadata, refused ACP sessions outright, and dispatch sessions
rejected images while a turn was running. All three came from the same
root cause: the steering channel was text-only, so anything richer had to
wait for a turn boundary.

Widen the channel instead of gating on it. `AgentDialogSteerRequest` and
`RoundInjection` now carry `attachments` and `metadata` — the same payload
shape a turn submission carries — and the execution engine rebuilds them
into a multimodal user message at the round boundary, so a steering
message with images reaches the model exactly as it would have at a turn
boundary. A malformed attachment is rejected up front by the scheduler
(while the caller still holds the message) rather than dropped later.

The dispatch append path carries attachments end to end as well, under
the same structural and device-inline limits `dispatch_continue` already
enforces.

On the client, "send now" is available in every session state:

- a running turn takes the message through steering, payload intact;
- a turn that ended between the click and the request no longer surfaces
  a failure — it falls through to the drain path;
- ACP agents own their execution loop and expose no mid-turn injection
  point, so the button moves the message to the head of the queue and
  the IDLE drain listener sends it the moment the turn ends.

The steering bubble now renders its attachments, so an injected message
looks the same as one sent from the composer.
…ache

fix(ci): isolate sherpa native cache lifecycle
Scheduled jobs were only reachable from the workspace or session that
owned them, so there was no place to see what the whole install is about
to run. A Todos entry under the Assistant nav button now opens a scene
that aggregates jobs across every workspace in two tiers: runs due within
24 hours as a list, runs further out on a month calendar with a
click-through day detail. Todos can be created and edited in place, which
also gives manual creation an entry point that does not require opening
the owning workspace first.

Todos are scheduled jobs rather than a parallel concept, so the scene
reads and writes the same cron service the agent's scheduling tool and
the per-workspace editors use, and broadcasts on the shared change event
so those views stay in step in both directions. A job whose next run is
gone is grouped as paused, done, or invalid instead of disappearing, so
it stays reachable.

Occurrence times are derived the way the backend derives them, mirroring
the Rust `cron` crate rather than classic crontab: 5/6/7 fields,
day-of-week 1-7 with Sunday as 1, and day-of-month ANDed with
day-of-week. Getting those wrong would advertise runs that never happen,
so they are pinned by tests. Schedules carrying an explicit timezone are
not expanded client side — a browser's only timezone database is `Intl`,
which the i18n contract reserves for the shared formatters — so they fall
back to the next run the scheduler itself reports rather than an
approximation. Each tier caps occurrences per job, per day on the
calendar, so a five-minute job marks every day it runs instead of
flooding one.

Also extracts the pure draft and schedule conversion helpers out of
ScheduledJobsView into scheduledJobDraft.ts so both editors build their
requests from the same code. That move is mechanical, with no behavior
change to the existing view.
- map common external Agent tools to provider-neutral BitFun capabilities
- add owner-scoped bulk controls with exact reviewed decision sets
- surface safe MCP startup failures and actionable source states
wgqqqqq and others added 24 commits August 18, 2026 09:55
…ror-handling

fix(harmonyos): wrap SDK BusinessError so ArkTS compile warnings clear
Two provider ids serving the same models is a real deployment, not a
mistake: a second route to the same vendor, mounted under its own id so
it can carry its own key. The picker listed the catalog per provider, so
a user with such a route saw every model twice — same name, same vendor,
nothing to choose between, and only the value prefix telling the rows
apart.

Publish one row per model id. The provider carrying the session's
current model owns the shared ids, so the list stays on the route that
actually answers, and the checked row keeps its provider even when that
provider's listing failed. A provider left with no models of its own
drops out of the picker; one that has models nobody else serves keeps
its group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An ACP agent publishes its modes as a `mode`-category config option, and
they were rendered inside the model dropdown, under the model list and
above the fast-mode switch. Two unrelated choices shared one button: the
trigger named the model, so nothing on the composer said which mode was
in force, and opening the model list to change the permission mode read
as the wrong menu.

Split the mode into its own trigger beside the model, built on the same
dropdown, keyboard, and positioning code as the reasoning preset picker,
so the composer now reads model, mode, reasoning. The trigger shows the
mode name. Protocol behaviour is untouched: the same
`session/set_config_option` call, the same lock semantics once a
conversation has started, the same option ids and test hooks.

An agent that publishes a mode and no models keeps a full picker — the
mode trigger takes over the context-usage badge and the tooltip that the
model trigger would have carried, so nothing is lost when the model
button is not rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
feat(dsh-acp): offer a per-session model picker
Creating a session in a workspace other than the open one first sent
set_workspace, so the phone moved the desktop's own view before the create
landed. CreateSession already binds whatever workspace_path it is handed, so
that round trip bought nothing and cost the desktop its place — harmless
enough with one machine in view, wrong once the sidebar lists several.

Carrying the path on CreateSessionOptions collapses the three-branch dance
around it, along with the recovery path that existed only to undo a host
workspace this no longer touches.

Co-authored-by: Cursor <cursoragent@cursor.com>
The remote surface only ever faced one machine: the workspace section drew
the desktop the phone was currently driving, and reaching another one meant
moving the global control target, which took the first one out of view. A
console that can only see where it happens to be pointed is not a console.

deviceRpc is already stateless — every call carries the relay URL, session
and target device — so a read-only directory can fan out without touching
RemoteSessionManager's single transport, which still owns the conversation.
Expanding a device is what dials it; offline rows are never dialed at all,
which is what keeps opening the drawer off a row of 130s timeouts.

Each device gets its own SidebarDeviceGroup because SessionListProjectionCache
holds a single slot keyed on the workspace, so one instance serving N devices
would miss on every rebuild. Catalogs persist per device_key and the live
device's own catalog is captured on publish, so a device keeps its rows after
the control target moves on.

Co-authored-by: Cursor <cursoragent@cursor.com>
Scoped to the HarmonyOS app: what was verified against main, why the
observation surface fans out per device while the conversation surface stays
on one, and why cross-device execution status is out of reach until the
remote SessionInfo payload carries a status field at all.
…ooked at

Device presence was fetched once at cold start and never again. The relay
keeps presence in a live socket registry, so its answer is current whenever
it is asked — the phone simply stopped asking, and a desktop that came
online after launch stayed grey in the sidebar until the app was killed.

Returning to the foreground and opening the sidebar each refetch, with a
15s freshness window so the two firing together on a cold start cost one
request rather than three.
Tapping a session on another desktop tears the current link down before
dialling the new one, so a desktop that has gone away used to cost the user
both machines for two minutes: AccountDeviceCommandTransport dropped the
caller's timeout on the floor and always took deviceRpc's hardcoded 130s,
nothing on screen said a switch was underway, and when it finally failed the
phone was left on no device at all with polling and the heartbeat stopped.

The timeout now travels from the transport through to deviceRpc, and the
connect handshake passes 15s of its own — a desktop that is running answers
get_workspace_info from memory, so silence there means it is gone, not busy.
The switch seam shows a toast, and a failed dial restores the device it
started from, snapshotting it before the teardown makes it unreadable.

Also from the same round on the device: the active desktop rendered grey
because the ForEach key for the unpaired placeholder collided with the real
row for that same machine, freezing the child on an object nothing updates;
and a live link now outranks the relay's presence answer, which is hearsay
from a socket registry that lags. Verified against two desktops, the failure
path reproduced by freezing one so the relay kept reporting it online.
The phone decides; it never executes. Detached Dispatch already has the
shape this needs — the phone remote-controls A, A is the controller that
owns the Git baseline, and B is the target that runs the worker. What was
missing was never authority, only an entry point on the phone.

So the design is written as a checklist of decisions, not of features: for
every decision the desktop can make about a job, the phone needs a verb,
and every desktop command left out has to say why it is work rather than a
decision. What cannot travel is the conversation — the submit request
carries no history, on any client — and that boundary is stated rather
than papered over.
…vice-directory

feat(harmonyos): show every account desktop in the sidebar
The in-app language never changed because RemoteI18n only had a
hardcoded zh-CN map and no locale state. Keep catalogs, persistence,
and Settings selection on the phone so the current surface rebuilds
immediately after a switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Vendors flashgrep v0.2.15, aligns BitFun with the daemon's new base-snapshot
protocol, and closes the gaps that made the managed index either wrong or
inscrutable from the UI.

Protocol (breaking upstream change): `RepoStatus.rebuild_recommended` is gone,
replaced by base-delta fields; `SearchParams.allow_scan_fallback` and the
BitFun-only `QuerySpec.before_context`/`after_context` are gone too. The old
required field would have failed every status-bearing response, so the binary
bump and the protocol change land together. The "rebuild recommended" badge
becomes "index catching up", driven by `base_advance_target_head`.

Line text: the daemon's four search modes return positions only, so BitFun was
rendering line numbers as content ("path:73:line 73"). Content search now goes
through `search/grouped_line_matches` and hydrates text from disk, locally via
`workspace_search/line_hydration.rs` and over SSH via a single batched awk pass
(`remote_line_hydration.rs`) rather than one round trip per file. Both paths
share `services-core::filesystem::content_preview`.

Auto-index policy: index only workspaces with roughly 2000+ indexable files.
Below that the measured win is 8 ms to 0.2 ms while the index inflates worst
(3.3x). The file count is gathered in two commands because
`--others --exclude-standard` costs 4.2 s on chromium against 89 ms for
`--cached`, so the tracked pass returns early once the threshold is met.
Builds run through a queue with a cross-workspace disk budget.

Policy visibility: the daemon reports `needs_index` both while the policy is
still evaluating and after it declined, so the UI could only hedge. The
decision now rides along with the status (`WorkspaceIndexStatus.auto_index`),
and a small workspace reads "no index needed" with the count and threshold
instead of an ambiguous sentence that never changes. Remote workspaces have no
BitFun-side policy and report `None`.

A workspace that is not a Git worktree can never be indexed, which is a
property of the folder rather than a fault, so it stays on the neutral
indicator instead of turning red.

Grep's `-A`/`-B`/`-C` route to ripgrep, because the daemon has no context-line
support and the flags were previously dropped on the wire without a word.
`ContentSearchRequest` loses its context-line fields entirely rather than
keeping them unread: a field that looks like it carries context but is silently
discarded is exactly how the original defect happened, so the indexed path can
no longer express the request at all.
…ex-strategy

feat(search): make the workspace index policy visible and correct
feat(harmonyos): switch the phone UI between Chinese and English
Project the resolved per-round permission mode into tool custom data so
Task delegation forwards the parent's effective mode instead of falling
back to the global default.

Extend tool-context materialization coverage for auto_approve mode.
…fixes

fix(sessions): stabilize durable history reconciliation
Align the code-repo docs with architecture/guideline/specs/plans, externalize user guides to bitfun_doc, and keep GCWing norms via guideline ops pages plus path-only product/audit reference updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep product-facing setup docs in the code repo and retarget README / RemoteConnectDialog links away from bitfun_doc.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove gitcode/bitfun_doc links and out-of-repo placement rules so governance docs focus only on in-repo documentation scope.

Co-authored-by: Cursor <cursoragent@cursor.com>
@GCWing

GCWing commented Aug 20, 2026

Copy link
Copy Markdown
Owner

The document status is inconsistent with the current repository, and since the document affects AI behavior—yet repository-side maintenance of it will be reduced going forward—this PR will not be merged.

@bobleer
bobleer requested a review from GCWing August 20, 2026 02:28
@bobleer
bobleer marked this pull request as draft August 20, 2026 02:29
@GCWing GCWing closed this Sep 5, 2026
@GCWing

GCWing commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Thank you for your contribution.

The main branch has been replaced with the 1.0.0 codebase from 1.0.0-explore. As part of this migration and Git history cleanup, the affected pull requests have been temporarily closed.

If your changes are still needed, please reapply them on a fresh branch based on the new main, then open a new PR against main or update and reopen this PR. Please link any replacement PR to this one so we can retain the discussion and review context.

We apologize for the disruption and appreciate your understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants