The lead currently leaves teams alive after the task list drains because
none of the prompt surfaces tell it WHEN to close or HOW. omx-style
'self-closing' behavior was missing for four reasons (diagnosed via
prompt-engineering A/B/C: wrong / misframed / missing):
1. builtin team-mode skill 'Lifecycle' (B+C): 'phase ends / shape
outgrown' is qualitative, so the model maps it to 'wait for user'.
Step 6 jumped to team_delete without the request/approve pair the
tool contract requires. Replaced with a 'Closure Contract' (a
computable predicate over team_task_list + team_status) and an
explicit 'Closure Sequence' (request -> approve -> delete, with
force=true reserved for unrecoverable paths only).
2. TEAM_MESSAGE keyword injection (C): spent 100%% of its one-shot
budget on routing ('do not substitute delegate_task'), 0%% on
closure. Added the same closure rule in compressed form. Kept the
'NEVER substitute with delegate_task' literal that
keyword-detector/index.test.ts depends on.
3. team-mode-status-injector body (C): the only per-session injection
for team mode had no closure obligation. Replaced the optional
'load the team-mode skill ... otherwise use the team_* tools'
sentence with a 'Closure invariant' clause that ties the check to
every team_task_update.
4. member-guidance Wrap-up (A+B): step 3 said 'so the lead can decide
whether to request shutdown', but team_shutdown_request is
lead-only - members cannot initiate it. Step ordering also placed
the completion message before team_task_update, so the lead's
closable check would see stale data. Reordered to
task_update -> check task_list for new work -> if nothing left,
send a single 'closure-ready' message and idle. Test assertion
updated to match the new accurate contract.
Also: stripped Korean alternation from TEAM_PATTERN per directive
('절대로 코드 내에 한국어 적지 마라'). Pattern is now
/\\bteam[\\s_-]?mode\\b/i. Removed 4 Korean test cases
(2 positive triggers + 2 false-positive guards) that the pattern no
longer needs to defend, and updated the keyword-detector AGENTS.md
row.
Net: -71 lines across prompt surfaces. The Closure Contract is the
only addition; everything else tightened.
Tests: 428/428 pass across src/features/team-mode/,
src/features/builtin-skills/, src/hooks/keyword-detector/,
src/hooks/team-mode-status-injector/, src/hooks/team-mailbox-injector/,
src/hooks/team-tool-gating/, src/hooks/team-session-events/.
LSP: no errors introduced (one pre-existing error in
keyword-detector/index.test.ts confirmed pre-existing on dev).
Ship the documented Team Mode security-research capability as an .agents skill and expose /security-research through transition command wrappers.
The exact security-review slug has no reachable history hits; security-research is the documented missing artifact from README and issue #3887.
Refs #3887
Plan: plans/security-research-restore.md
In v4.1.0+, users observed duplicate assistant streams rendering the same
content in two languages simultaneously (e.g. Chinese + English), most often
at the end of a turn.
Root cause: ParentWakeNotifier.requeueWake() unconditionally requeued ANY
wake that arrived during the background-agent-parent-wake post-dispatch
hold window. When a duplicate completion edge fired during that hold, the
same wake was replayed after the hold expired, triggering a second prompt
dispatch and a parallel assistant stream.
The fix compares the new wake against dispatchedParentWakes.get(sessionID)
and drops identical wakes during the gate hold, while preserving the existing
requeue behavior for genuinely-new wakes and failed-dispatch retries.
Regression test added in given/when/then style covering the duplicate-during-
hold scenario (TDD red-then-green).
Fixes#4256Fixes#4019
Same Web-Response-on-Node hazard existed in ripgrep auto-download flow,
zip extraction helpers, and binary downloader streams. Switch to the new
Node-safe reader and ensure no spawn path escapes as unhandledRejection.
Related to #3919.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
OpenCode Desktop v1.14.41+ runs OMO inside a Node.js utility process.
The previous `new Response(proc.stdout).text()` call is Bun-/Web-API-specific
and crashed the Desktop sidecar on Windows when grep/glob were invoked.
Switch glob/grep cli to the new process-stream-reader + search-process-output
helpers. Behavior on Bun and CLI/Linux/macOS is unchanged. ripgrep auto-download,
PowerShell fallback, and rgSemaphore are preserved.
Fixes#3919
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Introduces:
- src/shared/process-stream-reader.ts: Buffer-concat stream reader compatible with both Bun and Node ChildProcess stdout (replaces Web Response API usage)
- src/tools/shared/search-process-output.ts: structured subprocess output collector with timeout, kill, and rejection cleanup
- bun-spawn-shim hardened: Node path forces windowsHide: true; spawn errors no longer escape as unhandledRejection
Foundation for #3919 fix.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
OpenCode Desktop's Electron sidecar runtime can omit Bun's ctx.$ helper.
The sender previously called ctx.$ unconditionally, throwing
TypeError: ctx.$ is not a function as unhandledRejection and crashing
the sidecar with exit code 1.
Add a runtime guard at every call site, falling back to Node.js
child_process.execFile (with windowsHide: true) when ctx.$ is missing.
The Bun ctx.$ path remains preferred when available. Every notification
path is wrapped in try/catch so no failure escapes as unhandledRejection.
Fixes#4128Fixes#4061
The migration log message previously pointed users to docs/reference/configuration.md for the new LSP config location, but that doc section still showed the obsolete plugin-level 'lsp' block. A user following the guidance would re-add the same 'lsp' key, see it stripped again on next startup, and never reach a usable config.\n\nFix both sides: rewrite the log message so it is self-contained (states the new path .opencode/lsp.json and the consumer directly) and rewrite the LSP section in docs/reference/configuration.md to describe the actual current architecture (LSP served by the 'lsp' MCP server, reading server map from .opencode/lsp.json via LSP_TOOLS_MCP_PROJECT_CONFIG, schema lives in packages/lsp-tools-mcp).\n\nVerification: bun test src/shared/migration/ -> 26/26 pass. bun run typecheck -> exit 0. Manual probe -> migration still strips lsp from both in-memory and persisted file.
Issue 1: hasNewCommentsOnly() now returns false when oldString and newString
both contain comment syntax and the new lines are a subset of old lines —
preventing the hook from firing on comment-only modifications.
Issue 2: Per-session deduplication via sessionLastWarning Map with a 30s
window (DEDUP_WINDOW_MS). At most one warning fires per session per
response turn, breaking the deadloop on consecutive edits.
The lsp config block was removed from OhMyOpenCodeConfigSchema when LSP tools were migrated from native plugin tools to the lsp tier-1 MCP server (packages/lsp-tools-mcp). Zod v4 strips unknown keys silently on safeParse, so a v3-era oh-my-opencode.jsonc with 'lsp': { typescript: { command: ... } } continues to live in the file unchanged while doing absolutely nothing. The user reporting #4225 saw their custom LSP servers stop working with zero indication that the configuration site moved.\n\nAdd a migrator that removes the orphan lsp key during migrateConfigFile, mirroring the existing omo_agent -> sisyphus_agent migration immediately above and the 'Removed obsolete hooks from disabled_hooks' precedent below. A single log line records the configPath and the list of dropped server keys so the user has a paper trail in oh-my-opencode.log, and needsWrite is flipped so the cleanup persists to disk (with a timestamped backup) the next time the plugin loads.\n\nReproduction (clean upstream/dev, BEFORE fix):\n needsWrite=false\n inMemory.lsp=<original block, kept>\n persisted.lsp=<original block, kept>\n\nVerification (AFTER fix):\n needsWrite=true\n inMemory.lsp=undefined\n persisted.lsp=undefined\n\nbun test src/shared/migration/ -> 26/26 pass (24/24 pre-existing + 2 new regression tests). bun run typecheck -> exit 0.
The dist-side bug reported in #4220 (`path.endsWith(\"dist/cli.js\")` failing on
Windows backslash paths) has already been fixed in source by routing through
`hasCliSuffix` in `src/mcp/ast-grep.ts` and `src/mcp/lsp.ts`. The `cli-suffix.test.ts`
covered the lsp-tools-mcp shape but not the ast_grep shape.
Adds a regression case that asserts a Windows-style absolute path containing
`...\\packages\\ast-grep-mcp\\dist\\cli.js` matches both `\"dist/cli.js\"` and the
fully-qualified `\"packages/ast-grep-mcp/dist/cli.js\"` suffix — closing the
exact symptom from the bug report against future regressions.
`openai/gpt-5.3-codex` is the codex-series powerhouse still recommended
in docs/guide/agent-model-matching.md and listed in the default
fallback chain in docs/reference/configuration.md, not a deprecated
alias for `gpt-5.4`. The migration entry silently rewrote any user
config that picked `gpt-5.3-codex` for its token efficiency, sending
agents to a non-codex model on every startup.
Drop the bogus mapping from MODEL_VERSION_MAP and add a regression
test that explicit `gpt-5.3-codex` selections (including in nested
fallback_models) survive `migrateModelVersions`. Users already
auto-migrated previously can revert to `gpt-5.3-codex` by hand and it
will now stick on subsequent loads regardless of the sidecar history.
default-mode (system-transform):
- e5463e2db introduced auto-activation of ultrawork+ralph-loop, and
dc2e082ac then skipped the ultrawork system prompt whenever ralph_loop
was also enabled. Net effect: the keyword-detector still showed
'Default ultrawork mode enabled' to the user, but the first turn had
none of the ultrawork behavior. Loop continuation kept the ultrawork
prefix, so the contract was honored only on later iterations.
- Drop the skip so the initial turn matches what the toast advertises.
New matrix test pins all four (ultrawork, ralph_loop) combinations.
multimodal-looker:
- Prompt claimed 'read' and 'call_omo_agent' were available, but the
look_at invocation runtime explicitly disables both via READ_ENABLED
and createAgentToolAllowlist([]). Small VL models trusted the prompt
and looped on rejected tool calls (#4116).
- Rewrite the agent prompt to describe direct-attachment analysis and
forbid tool/agent calls. Add a consistency test that extracts the
prompt's 'available tools' claim and compares it against the
configured allowlist.
delegate-task (skill-resolver):
- 088693697 filtered per-agent restricted skills at the skill tool and
builtin agent prompt layers, but delegate-task itself happily injected
whatever skill name a caller passed. A target agent could be force-fed
a skill marked agent: oracle just by listing it in load_skills.
- Thread the target agent through resolveSkills and silently filter
skills whose definition.agent does not include it. Public skills with
no restriction are unaffected. Regression test pins the bypass.
notepad-write-guard:
- The hook was created by create-tool-guard-hooks but tool-execute-before
never invoked it, so the guard was inert.
- It also only matched .sisyphus/notepads, missing the current
.omo/notepads layout introduced by the workspace migration.
- Add the dispatch call alongside writeExistingFileGuard, and extend
NOTEPAD_ROOTS to cover both paths via normalize() + sep. New
integration test pins the wire and the .omo block; the existing unit
test now asserts both paths.
start-work session-plan-affinity:
- PLAN_PATH_PATTERN only matched .sisyphus/plans, so sessions referring
to plans under .omo/plans returned null and start-work missed the
current session's own plan.
- Extend the regex to .(sisyphus|omo)/plans and add findPrometheusPlans
in packages/boulder-state to scan both directories during the
transition. New regression test pins .omo/plans matching; legacy
.sisyphus/plans coverage preserved.
6ffea1bc3 added i18n with en/zh locales and plugin config support, but
the initI18n() call lived in the original src/index.ts. When src/index.ts
became an 18-line wrapper that delegates to
src/testing/create-plugin-module.ts createPluginModule(), the call site
was dropped on the floor. Result: i18n.locale config and LANG env both
ignored at runtime, every toast stayed English regardless of user
setting.
Inject initI18n as a managed dependency and call it in
createPluginModule() immediately after loadPluginConfig(), passing
pluginConfig.i18n?.locale through. Add an integration test that boots
the plugin with i18n.locale='zh' and asserts getLocale() returns 'zh'
and t('toast.task_completed') returns the Chinese string. Regression
locked - subsequent moves of the startup path will fail loudly.
- prompt-async-gate.test.ts: refactor ced36bffc removed
promptAsyncAfterSessionIdle in favor of the unified
dispatchInternalPrompt({ mode: 'async', ... }). One call site at
line 1441 was left behind. Replace it with the current API and pass
the explicit dispatchTimeoutMs so the status-timeout semantics are
preserved. Also switch the surrounding tests to the third-argument
timeout form so Bun's typings stay happy.
- runtime-model-readers.test.ts: implementation moved to
packages/model-core during the layering refactor; the orphaned test
still pointed at './runtime-model-readers'. Switch to the package
export via getModelCapabilities and keep the modality-reader
coverage by deriving keys through the package API.
c85d2f9bc added 'server_error' and 'an error occurred while processing'
to the retryable message patterns to fix#3799 - the OpenAI streaming
server_error case where runtime fallback never fired. The package
layering refactor (2748009ff) moved model-error-classifier into
packages/model-core but dropped these two patterns during the move.
Restore both patterns at the matching position. packages/model-core
tests now pass on the server_error retryable assertion, and runtime
fallback once again retries the OpenAI streaming server_error envelope.
mock.restore() in afterAll is global in Bun, so the previous attempt to
satisfy mock-module-lifecycle-audit by pairing mock.module('./logger')
with afterAll(() => mock.restore()) tore down mocks owned by other
tests. Concretely, running this file before src/hooks/runtime-fallback
produced 42 cascading fallback test failures.
The logger mock was decorative - tests do not assert log calls, and the
real logger only writes to oh-my-opencode.log in the OS temp dir. Drop
the mock entirely so the audit has nothing to score and other tests
keep their mocks intact.
- prometheus-prompt.test.ts: close missing }) on the OpenSpec expanded
commands describe block (introduced by d66b6bcbf, parse error).
- agent-sort-shim/agent-config-integration/continuation-injection/
unstable-agent-babysitter/subagent-resolver/sync-executor/
resolve-caller-team-lead tests: expect 'Sisyphus - ultraworker'
(lowercase) to match production after cd39f8858, which lowercased the
display name to dodge a TUI ZWSP rendering glitch. Legacy uppercase
inputs that exercise the normalization path are preserved.
- sync-executor.ts + resolve-caller-team-lead.ts: route legacy display
name inputs through normalizeAgentForPrompt so prompt agent names and
caller team lead lookups produce the canonical lowercase form.
bun run build:schema brings assets/oh-my-opencode.schema.json back in sync with the Zod source. Surfaces two fields that were already present in src/config/schema/ but missing from the published artifact:
- disabled_providers at the top level (from feat/config-disabled-providers)
- displayName on every agent override (from fix/schema-preserve-custom-agent-overrides)
No Zod source changes; only the generated artifact moves.
src/hooks/comment-checker/apply-patch-edits.ts was already a pure re-export over @oh-my-opencode/comment-checker-core after the core extraction landed. Every importer now reaches into the package directly, so the shim has no remaining call sites and can be removed.
Verified: rg "comment-checker/apply-patch-edits" src/ packages/ returns no matches.