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).
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
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.
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.
- 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.
- 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.
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.
Promote the project-rule constants (PROJECT_MARKERS, PROJECT_RULE_SUBDIRS, PROJECT_RULE_FILES, OPENCODE_USER_RULE_DIRS, USER_RULE_DIR, GITHUB_INSTRUCTIONS_PATTERN, RULE_EXTENSIONS, AGENTS_FILENAME, etc.) and the findAgentsMdUp walk-up helper out of the agents-md-core and rules-injector adapters and into @oh-my-opencode/rules-engine, the single owner of rule discovery.
- packages/agents-md-core/ drops the findAgentsMdUp/AgentsMdDiscoveryInput wrappers (now sourced directly from rules-engine) and its constants module re-exports AGENTS_FILENAME from rules-engine instead of duplicating it.
- src/hooks/directory-agents-injector/finder.ts pulls findAgentsMdUp from rules-engine directly while still re-exporting resolveFilePath from agents-md-core.
- src/hooks/rules-injector/constants.ts becomes a pure re-export shim over the rules-engine constants.
Add packages/agents-md-core/src/injector.test.ts to lock the root-skipping AGENTS.md injection order so future changes to findAgentsMdUp cannot silently regress the [Directory Context: ...] block format the injector emits.
Tests: bun test packages/agents-md-core packages/rules-engine src/hooks/directory-agents-injector src/hooks/rules-injector
The hyperplan trigger \b(hyperplan|hpp)\b/i matched 'hpp' inside common C++ header references like 'check interface.hpp' or 'open buffer.hpp'. The leading '.' is a non-word character, so \b is already satisfied and the false positive fires the hyperplan-mode prompt on routine code questions.
Split the alternation so 'hpp' additionally requires that the preceding character is neither a word character nor a '.'. This preserves every existing trigger ('hpp do this', '/hpp ...', mid-sentence usage, mixed case) while rejecting filename uses of the .hpp extension. The longer 'hyperplan' keyword keeps the original \b boundary semantics.
Reproduction (added regression tests):
- 'please help to check interface.hpp' must NOT fire
- 'open src/include/audio/buffer.hpp and fix the leak' must NOT fire
All 14 cases in hyperplan.test.ts pass (12 existing + 2 new), broader keyword-detector suite stays green (92 pass), typecheck clean.
When the working directory contains a .git folder the OpenCode server
normalises the project root to the git root before persisting messages.
This creates a race: the 429/503/529 error event can fire before the
user's message is committed to storage, so session.messages returns []
and getLastUserRetryParts returns an empty array. The previous code
treated that as a silent no-op (cleared all retry state, Sisyphus stalled).
Fix: when fetchedParts is empty, emit a structured log explaining the
.git-directory race and fall back to a synthetic { type:"text", text:"continue" }
part — matching the pattern already used by autoContinueAfterFallback in
event.ts. The fallback dispatch always proceeds regardless of whether
the messages API can return user parts.
Update four tests that fired two consecutive session.error events relying
on the old silent-stop behaviour: add top-level model fields to the second
error so the awaiting-fallback gate recognises it as coming from the
dispatched fallback model and lets it through normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PLANNING_CONSULT_WARNING was prepended to the prompt forwarded to
subagent LLMs via task(). Its leading bracket-enclosed marker
`[SYSTEM DIRECTIVE: OH-MY-OPENCODE - PROMETHEUS READ-ONLY]` is
exactly the indirect-prompt-injection signature that Azure OpenAI
Prompt Shield flags in user-role content; on GPT-5.4 through
Azure, the model returns "I'm sorry, but I cannot assist with
that request." before any planning work runs, making Prometheus
non-functional on Azure.
The bracket marker was designed for internal hook-to-hook
filtering, but PLANNING_CONSULT_WARNING leaks it to external LLM
payloads. Replace the header with a neutral XML-tag wrapper
(`<planning-context source="prometheus-read-only">`) that Azure's
filter does not match while preserving the human-readable warning
body. Internal isSystemDirective() consumers are unaffected.
Regression test asserts the post-hook task() prompt does not
begin with the flagged bracket sequence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Strengthen Prometheus plan template with FORMAT constraint
- Add task label format check to Oracle phase-2 (N/6 → N/7)
- Add format checks to self-review checklist
- New plan-format-validator hook: compares raw checkbox count
against getPlanProgress() after plan writes, warns agent when
labels are malformed (0/0 or partial skip scenarios)
- P1: When allowedEnvVars is provided, PATH was missing from the base
restricted env, causing non-builtin commands to fail at exec time
- P2: Allowlisted HOME/CLAUDE_PROJECT_DIR could overwrite normalized
values from getHomeDirectory()/cwd with ambient process.env values
- P2: Test suite mutated shared pluginHooksState singleton without
resetting it in afterEach, causing cross-test state leaks
- Apply mcp_env_allowlist to plugin hooks: intersect HTTP allowedEnvVars
with MCP allowlist, set command allowedEnvVars to full MCP allowlist
- Scrub process.env in executeHookCommand when allowedEnvVars provided
- Add PluginHooksState class with per-directory Map storage
- Add PluginHooksConfig interface for typed boundary layer
- Pass directory context through hook-config-handler
- Add 16 tests across 4 files (40 assertions) covering allowlist
filtering, env scrubbing, directory isolation, and edge cases
- Remove unnecessary 'as' type assertions, use discriminated union
narrowing instead
Todo continuation could reinject with a lowercase built-in config key such as hephaestus when no registered alias was available. OpenCode prompt dispatch expects the prompt-facing agent name, so the continuation failed with an agent-not-found error.
Normalize the dispatch fallback through the existing prompt agent display-name resolver and pin the lowercase Hephaestus regression.
Gate idle ready notifications on the existing background-task continuation marker so cmux does not receive premature ready alerts while delegated work is still active.
Constraint: Reuse marker state from background task lifecycle without adding new notification config
Rejected: Patch cmux directly | notification readiness belongs upstream in OMO
Confidence: high
Scope-risk: narrow
- Drop the back-import of src/shared/logger so @oh-my-opencode/rules-core
stays free of host-adapter dependencies (ROADMAP package layering
invariant). Expose setSisyphusRuleDeprecationLogger(logger) for hosts to
inject their own logger; default is a noop.
- Wire the host injection in src/hooks/rules-injector/rule-file-finder.ts
as a module-level side effect so existing behavior is preserved.
- Add realpath boundary check to findRuleFilesRecursive and
validFileRealPath. Project rule scanners now refuse entries whose
realpath escapes the rule-source root, closing the symlink-escape
vector where a malicious repo could point .github/copilot-instructions.md
(or any .omo/rules/* entry) at ~/.ssh/id_rsa and have the rule injector
pull the secret into model context.