Skip to content

feat(host): add WorkBuddy as a first-class init agent - #99

Open
cioerp wants to merge 2 commits into
aoci-spec:mainfrom
cioerp:feat/workbuddy-host
Open

cioerp wants to merge 2 commits into
aoci-spec:mainfrom
cioerp:feat/workbuddy-host

Conversation

@cioerp

@cioerp cioerp commented Oct 4, 2026 •

Copy link
Copy Markdown

Design discussion: #98

What changes and why

WorkBuddy was not a recognised host. Three concrete consequences: init --agent workbuddy failed closed, doctor had no row for it, and — most consequential —
the managed AGENTS.md block is appended at the end of the file, while
WorkBuddy injects only the first few thousand characters of that file into model
context. A trailing block is therefore never read there, so the single written
instruction telling a model to load aoci_rules and aoci_overview before
starting work is structurally unreachable on that host.

WorkBuddy is also structurally different from every existing host: it has no
project-scoped MCP surface
. Its only entry is the machine-level
~/.workbuddy/mcp.json (read and written by UserMcpProvider); ./.mcp.json is
only consulted when a plugin declares it. That file is shared by every project
on the machine while an aoci server is bound to one --repo, so copying the
Claude implementation would make init for repository B silently repoint
repository A's server.

This change adds the host with that constraint respected rather than worked
around: merge into the machine-level file, never overwrite an entry that belongs
to another repository, write nothing into the repository, keep --hooks inert
instead of emulated, and prepend the managed block for this host alone.

Affected public contracts

  • 无公开合同变化
  • MCP 工具名、输入 Schema 或响应结构
  • CLI 命令、旗标、退出码或 --json 形状
  • spec/public/ 合同文本或机器词表(internal/machinecontract)
  • 索引格式、Baseline、收据或事务身份推导

The nine MCP tool names, their schemas and the binary name are untouched.
The CLI change is
init --agent accepting workbuddy, plus a new doctor row and a new
integrations key in the read-only UI snapshot.

spec/public/ is deliberately untouched: it specifies host capability,
interaction and delivery contracts and does not enumerate hosts, so no public
contract text changes. All user-visible strings live in the Locale assets, and
both en-US and zh-CN gain the same keys.

Compatibility: --agent all keeps its existing claude/codex/cursor set. That set
means "install project-level host configuration", and silently widening it would
change what all does for every existing user. An older binary that meets a
workbuddy key in ~/.workbuddy/mcp.json behaves exactly as it does today — it
ignores unknown keys in a file it does not manage.

Verification

  • make fast
  • make full
  • python3 scripts/blackbox/mcp_conformance.py
  • python3 scripts/blackbox/mcp_scenarios.py
  • python3 scripts/blackbox/mcp_lifecycle.py
  • 其他:端到端(隔离 HOME 的临时仓库)+ 变更包的对照实验

make fast was run on macOS. Two environment facts, both pre-existing and
unrelated to this change:

  • macOS exposes /var as a symlink to /private/var, and t.TempDir() returns
    a /var/folders/... path. Roughly 70 tests then fail with
    原子创建父路径不安全: /var or safe_inventory_git_boundary_mismatch. With
    TMPDIR=/private/tmp all of them pass — including internal/cli, the largest
    package, which goes to 0 failures.
  • internal/fs TestProcessIdentityIsStableAndNonEmpty still fails on macOS:
    process_identity_unix.go is built for darwin and returns known=false, which
    the test asserts against. On Linux the _linux implementation reads
    /proc/<pid>/stat, so CI is unaffected. This package is not touched here.

Beyond the gate:

  • Control experiment on internal/cli, the package with the most surface area:
    identical failure sets before and after, compared by test name. Comparing
    whole lines is useless because every line carries a duration.
  • Ten new tests in internal/hooks/workbuddy_test.go cover idempotence,
    foreign-entry preservation, scoped-key conflict, broken-JSON refusal, existing
    config preservation, the "another repository is not installed here" predicate,
    Detect, dispatch through Install, key sanitisation, and both block
    placements. A second test pins that the default placement is still the append.
  • End-to-end against a throwaway repository with an isolated HOME: the entry
    lands in the user file with the right command and --repo; the working tree
    keeps no .mcp.json, .workbuddy, .claude, .codex or opencode.json; a
    second repository receives aoci-<project> while the first entry stays
    byte-for-byte intact; doctor reports the WorkBuddy row; the managed block
    lands on line 1 with existing content preserved; a second run changes nothing.

Operating system impact

The installer only writes a JSON file, and path handling reuses the existing
TplData forward-slash normalisation, so Windows behaves the same as it does for
the other hosts. The host itself was exercised on macOS only, and the 8000
character injection limit quoted in the code comments and docs is a measured
property of that host, not a portable constant.

Migration and recovery

No persisted data changes. The only new persistent state is one additional key in
an existing user-level JSON file, written through the same backup-then-atomic-write
path every other installer uses. Removing that key is the complete rollback.

A note for operators already running an older build: overwrite a running
executable with rm plus cp, not cp alone. Replacing the bytes under a live
process left the new binary failing to start with exit code 137 and no output,
while the same file ran normally from its build directory. The old inode stays
alive for the running process, so delete-then-create is the reliable order.

Cognition

  • 受管对象有变化,已完成 AOCI 治理循环(maintain → update_entry → verify/check),且索引与代码在同一提交内
  • 无受管对象变化

Neither box is ticked, and that is accurate rather than an omission. This
does change managed objects: workbuddy.go is new, and five Go files plus two
Locale assets changed. The first box stays unticked because the governance loop
is not done: it runs through the MCP tools, which are bound to whichever
repository the host started them for, so a contributor working in two checkouts
cannot complete it for the second one without restarting the host — and this
repository's own aoci check does not start clean. Raised as an open question
in #98 rather than answered by guess. Happy to add the entries in a follow-up
commit if that is the preferred order.

MindIniter added 2 commits October 4, 2026 21:14
WorkBuddy has no project-scoped MCP configuration surface. Its only entry
is the machine-level ~/.workbuddy/mcp.json, read and written by
UserMcpProvider. That file is shared by every project while an aoci MCP
server is bound to one --repo, so writing a fixed "aoci" key would silently
repoint another repository's integration.

- workbuddy.go merges into ~/.workbuddy/mcp.json and never overwrites a
  foreign aoci entry: it falls back to aoci-<project>, and reports an error
  when that key is taken as well
- IsWorkBuddyMCPInstalled scans every entry for one bound to this
  repository, so another repository's aoci never reads as installed here
- Detect probes the user-level file; a project-root probe would always miss
- init --agent workbuddy writes no repository file, so
  initAgentCandidatePaths returns nil and .gitignore is left alone
- the AGENTS.md managed block is prepended for this host only: WorkBuddy
  injects roughly the first 8000 characters of AGENTS.md into model context,
  so a trailing block is never read. Every other host keeps appending
- --hooks is intentionally inert here; WorkBuddy exposes no pre-write
  lifecycle hook surface, and the adapter never pretends it installed one
- agent-integrations.md: new WorkBuddy section covering the machine-level
  configuration shape, the never-overwrite-foreign-entry rule, the inert
  --hooks, and the prepended managed block; the shared header now names the
  machine-level file and states why it is the exception to the commit rule
- README (en/zh-CN): host lists, prerequisites, host tables, configuration
  file inventories and the command samples
- troubleshooting.md: stale-entry list, the "tools appear in unrelated
  projects" section (WorkBuddy is the host where scoping cannot be achieved by
  moving a file), and the .gitignore boundary
- CHANGELOG: Unreleased entry
@alkor2000

Copy link
Copy Markdown
Contributor

I went through this properly, and it's not going into rc18. The design is right; a few things need fixing first, one of them before anyone runs the tests on Windows.

The blocker: workbuddy_test.go only sets HOME. On Windows os.UserHomeDir reads USERPROFILE, so the tests write into the real %USERPROFILE%.workbuddy\mcp.json, and two of them overwrite it with {not json and [[[. Our CI doesn't run internal/hooks on Windows, so nothing caught it. Setting USERPROFILE too, or making the home resolver injectable, fixes it.

Things I reproduced: a file containing null panics; "mcpServers": [...] is silently replaced while the output says existing servers were preserved; a moved or upgraded aoci binary makes the same repository look foreign, so WorkBuddy ends up with two servers on one repo; every non-ASCII project name becomes aoci-repo; two concurrent inits can lose an entry (BackupThenWriteCAS exists for this).

Two docs points before it lands. The en-US AGENTS block is about 15,600 characters, so with the 8,000-character window you measured it gets cut in half even when it's first, and a repo already initialised for Claude or Codex keeps its block at the end. Please also put the WorkBuddy version you measured in the docs; right now the README lists it next to hosts we validate ourselves.

Smaller: the UI page hard-codes its integration keys, so the new key never renders; "byte-for-byte intact" isn't true after MarshalIndent, "semantically intact" is; the HOME-missing message should mention USERPROFILE on Windows.

Fix those and I'll land it on the 0.2.0 line under your name.

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.

2 participants