Reviewed in OMX. The PR stays narrow: it does not pretend to fix the reporter's entire marketplace/cache path, but it does lock the verified npm pack surface contract so hook-referenced scripts cannot silently disappear from the published package again.
Re-reviewed in OMX after follow-up commits. Original blocker was contract drift between hook-side alias acceptance and end-to-end routing proof. The updated PR narrows the routing proof to CC-native envs, excludes OMC_MODEL_* from alias proof, and keeps OMC-owned vars on the stricter safety path. Targeted hook/model-routing tests passed locally and CI is fully green.
Reviewed in OMX. Initial blocker was stale shipped artifacts; follow-up commit aligned bridge/dist outputs with the updated built-in Opus default, and CI is now fully green.
Bumps all version files to 4.12.0. Includes 6 new features, 67 bug
fixes, and 3 other changes across 121 merged PRs since v4.11.3.
Highlights:
- feat(team): per-role provider and model routing (#2614)
- feat(hud): narrow elementOrder config for main-line layout (#2655)
- feat(ci): upgrade test to catch install/update regressions (#2635)
- fix: align stop hook to persistent-mode.mjs + output contracts (#2653)
- fix(session-start): stale CLAUDE_PLUGIN_ROOT symlink resolution (#2630)
- fix(pre-tool-enforcer): OMC_STATE_DIR centralized resolver (#2621)
- fix(keyword-detector): ignore pasted transcript blocks (#2631)
- test(skills): isolate bridge-rewrite test from Claude runner env leakage
Scope-risk: broad
Confidence: high
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue #2654 asks for a settings-level way to reorder the main HUD line,
but the repo already has a broader layout system. This change adds a
narrow top-level `elementOrder` convenience setting that preserves the
existing defaults, defers to `layout.main` when advanced layout is in
play, and keeps unknown names harmless.
Constraint: Existing HUD users must see no behavior change unless elementOrder is set
Constraint: layout.main remains the authoritative advanced ordering surface
Rejected: Add elementOrder under omcHud.elements | blurs enablement config with structural ordering
Rejected: Replace layout with a new ordering model | too broad for the issue scope
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep elementOrder limited to main-line ordering unless a future issue explicitly expands the contract
Tested: npm test -- --run src/__tests__/hud/render.test.ts src/__tests__/hud/state.test.ts
Tested: npm run build
Tested: npx eslint src/hud/types.ts src/hud/render.ts src/hud/state.ts src/__tests__/hud/render.test.ts src/__tests__/hud/state.test.ts
Not-tested: Manual HUD rendering inside a live Claude Code session
Co-authored-by: Codex Review <codex-review@example.com>
The HUD watch path polls git status on a 1s loop, so using the default status invocation can contend with writers that need .git/index.lock. Switch the read-only statusline command to disable optional locks without changing parsing or presentation behavior.
Constraint: Keep the fix limited to the HUD git status polling path
Rejected: Change all git status calls globally | broader behavior change than issue scope requires
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep read-only HUD/statusline git probes lock-free unless a future feature explicitly requires index writes
Tested: npm run test -- src/hud/__tests__/git-status.test.ts src/__tests__/hud/git.test.ts; npm run build
Not-tested: Runtime reproduction against a live concurrent index writer
Deep-interview already injected the configured ambiguity threshold into the rendered skill body, but builtin skill caching made that value sticky for the rest of the process. When settings.json changed, later deep-interview runs could keep using the old threshold as the initial prompt/state source even though slash rendering and the settings file had moved on.
Constraint: Fix the rendered/current-dev failing path without broad CLI-init redesign
Rejected: Rework deep-interview state initialization around a new CLI bootstrap path | broader than the confirmed bug and unnecessary once rendered-skill cache invalidates on threshold changes
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep rendered deep-interview threshold injection and builtin-skill caching keyed to the effective runtime settings so prompt state does not drift from settings.json
Tested: npx vitest run src/__tests__/skills.test.ts -t "deep-interview"
Tested: npx vitest run src/__tests__/auto-slash-aliases.test.ts -t "applies deep-interview threshold runtime injection in slash/materialized output"
Tested: npm run build
Not-tested: Manual interactive deep-interview session through Claude Code UI
The real regression path was /ccg slash-skill expansion, not direct askCommand.
Slash rendering rewrote advisor examples to the plugin bridge when omc was unavailable,
which sent codex/gemini advisor calls back through launch.ts inside an active Claude session.
Constraint: /ccg and other slash skills must keep working in plugin installs where omc is absent
Rejected: Special-case /ccg in the slash executor | broader ask rendering fix is narrower and preserves canonical skill content
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Preserve omc ask as the canonical in-session advisor path; do not rewrite ask invocations to the bridge inside Claude sessions
Tested: npm run build:cli; npm test -- --run src/__tests__/omc-cli-rendering.test.ts src/__tests__/auto-slash-aliases.test.ts src/cli/__tests__/ask.test.ts
Not-tested: Live manual /ccg invocation against installed plugin runtime
The prior threshold fix shipped in v4.11.6, but only the builtin-skill loader
applied the deep-interview runtime substitutions. Slash/materialized execution
read raw SKILL.md content directly, so users still saw stale 20%/0.2 strings in
the path they actually invoked.
This change centralizes bundled skill body rendering and reuses it from the
slash-command executor so deep-interview threshold injection cannot drift
between paths. It also extends the regression tests to cover the slash path and
one additional hardcoded ambiguityThreshold snippet.
Constraint: Fix must stay narrow and preserve existing skill behavior outside deep-interview threshold rendering
Rejected: Patch only the slash executor with another ad hoc string-rewrite block | would preserve the same two-path fragility
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep bundled skill body transforms shared across builtin and slash/materialized paths; do not duplicate deep-interview substitutions again
Tested: npx vitest run src/__tests__/auto-slash-aliases.test.ts
Tested: npx vitest run src/__tests__/skills.test.ts
Tested: npx vitest run src/__tests__/skills.test.ts src/__tests__/auto-slash-aliases.test.ts
Tested: npm run build
Tested: npx eslint src/features/builtin-skills/skills.ts src/hooks/auto-slash-command/executor.ts src/__tests__/skills.test.ts src/__tests__/auto-slash-aliases.test.ts
Not-tested: Full repository test suite beyond the targeted skill/slash regression coverage
Condense overgrown comments added during this PR's review loop:
- parseZaiResponse JSDoc: 17 lines → 6 lines, aligning with sibling
parseUsageResponse / parseMinimaxResponse (1-line) but keeping the
non-obvious purchase-date cutoff and unit-vs-reset-time rationale.
- ZAI_UNIT_WEEK constant: 5-line justification → 1 line.
- ZaiQuotaResponse.unit field: 4-line tri-mapping table → 1 line.
- Inline "Bucket assignment" 3-step numbered comment: removed; the
code and a 1-line tiebreak note carry the same information.
- sortByResetTime: dropped the ad-hoc generic parameter, reuse the
TokensLimit type alias so the helper matches the file's other
inline helpers (parseDate, parseResetTime).
No behavior change; 58/58 tests still pass.
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Record the actual product policy in the parseZaiResponse doc comment:
weekly TOKENS_LIMIT presence is gated by purchase date, not by tier
(`level: "pro"` alone does not imply a weekly bucket).
- Purchased before 2026-02-12 (UTC+8): single TOKENS_LIMIT only.
- Purchased on/after 2026-02-12 (UTC+8): two TOKENS_LIMIT entries
(5-hour + weekly).
This explains why both the no-weekly and with-weekly response fixtures
in the regression tests show `level: "pro"`.
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a regression fixture from a real pro-tier z.ai response that omits
the weekly TOKENS_LIMIT bucket (`level: pro` + a single TOKENS_LIMIT
unit=3 + a TIME_LIMIT unit=5).
Locks the contract:
- fiveHourPercent populated from the unit=3 TOKENS_LIMIT
- monthlyPercent populated from the TIME_LIMIT
- weeklyPercent and weeklyResetsAt remain undefined so the HUD's
`weeklyPercent != null` guard hides the `wk:` segment
Constraint: pro-tier alone does not guarantee a weekly bucket; users on certain pro plans receive only TOKENS_LIMIT (5h) + TIME_LIMIT (monthly)
Confidence: high
Scope-risk: narrow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewer (greptile) and repo maintainer flagged that sorting TOKENS_LIMIT
entries by absolute `nextResetTime` can swap the 5-hour and weekly buckets
in the final hours before a weekly reset — when the weekly window's
next-reset timestamp is actually smaller than the 5-hour window's.
Switch to `unit`-code-based classification:
- Entry with `unit === 6` -> weekly bucket
- Remaining TOKENS_LIMIT entries -> 5-hour bucket (tie-break by
nextResetTime ascending, then percentage)
If no `unit === 6` entry is present (older/unknown schema), fall back to
the previous nextResetTime sort so free-tier and legacy responses still
parse. A regression test exercises the bot's exact scenario (weekly
nextResetTime < 5h nextResetTime) and asserts unit-based mapping wins.
Constraint: z.ai does not document unit enum codes; mapping derived from an observed pro-tier response (unit=3 -> hour-based, unit=6 -> week-based)
Rejected: Keep nextResetTime-only sort | reviewer-confirmed bucket-swap risk near weekly reset boundary
Confidence: high
Scope-risk: narrow
Directive: If z.ai introduces a new TOKENS_LIMIT window class, extend the unit mapping — do not remove the nextResetTime fallback branch (it protects single-bucket / unknown-schema responses)
Not-tested: Response where all TOKENS_LIMIT entries omit the `unit` field entirely (fallback path is covered by existing legacy tests but not against a live API)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
z.ai API returns two `TOKENS_LIMIT` entries on pro+ tiers (5-hour + weekly),
but parseZaiResponse used `limits.find(type==='TOKENS_LIMIT')` and only
captured the first entry, silently dropping the weekly bucket. The prior
comment "z.ai has no weekly quota" was incorrect.
Fix: collect all TOKENS_LIMIT entries, sort ascending by nextResetTime.
The shortest reset window -> 5-hour slot; second window -> weekly slot.
Entries with missing/zero nextResetTime are pushed to the end so they
cannot steal the 5-hour slot. Equal reset times tie-break on smaller
percentage. When 3+ entries appear (future schema drift), the first two
by reset time win and a one-shot debug log is emitted under OMC_DEBUG.
Free/basic tiers (single TOKENS_LIMIT) are unaffected: weeklyPercent
stays undefined and the HUD `wk:` segment remains hidden by the existing
`!= null` guard in render code.
Constraint: z.ai does not document `unit`/`number` enum codes on limit entries, so disambiguation uses nextResetTime ordering rather than enum mapping
Rejected: Map `unit: 3 -> 5h` / `unit: 6 -> weekly` directly | undocumented enum, risk of silent breakage if z.ai renames codes
Rejected: Duration-threshold classifier (<= 6h -> 5h) | fragile against clock skew and post-reset near-zero duration
Confidence: high
Scope-risk: narrow
Directive: Do not narrow the `a.nextResetTime > 0` guard — `0` sentinel must disqualify entries from the 5-hour slot, test coverage exists
Not-tested: Real HTTP round-trip against z.ai pro tier; fixture test uses the actual response payload but not a live API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The workflow was failing because the final check depended on Claude CLI print-mode behavior and secrets availability instead of validating the installed SessionStart hook directly. This change switches the final step to repo-owned hook wiring and script execution checks so push and pull_request runs exercise the upgrade path without external runtime assumptions.
Constraint: Validation must avoid Claude CLI and work on pull_request events without secrets
Rejected: Keep as the trigger | brittle in headless CI and unavailable on pull_request
Confidence: high
Scope-risk: narrow
Directive: If hook output contract changes, update the JSON assertion here before reintroducing any CLI-based smoke test
Tested: python3 YAML parse of .github/workflows/upgrade-test.yml; git diff --check -- .github/workflows/upgrade-test.yml
Not-tested: Full GitHub Actions execution of upgrade-test workflow
P1: Remove HOME: \${{ env.HOME }} which resolves to empty string on
GitHub runners (env.HOME is not a workflow context variable), overriding
the runner's real home and breaking session-start hook path resolution.
P2: Widen stderr check to catch console.warn output (e.g. version drift
warnings) in addition to error/exception/fail patterns, by piping through
grep -v to exclude npm deprecation warnings before checking for omc
warnings/errors.
Constraint: secrets.ANTHROPIC_API_KEY unavailable on pull_request events
Confidence: high
Scope-risk: narrow
- Fork detection: use github.event_name != 'pull_request' instead of
github.event.pull_request.head.repo.fork == false (clearer, skips on all PRs)
- Version checks: capture omc exit code directly instead of via pipeline;
add empty-output guard after omc update
- Update step 2 comment to match new step 6 gate condition
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
npm install emits deprecation warnings to stderr which are not omc errors.
Only fail on actual error/exception/fail patterns, not npm transitive
dependency noise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Step 6 (claude --print) requires secrets.ANTHROPIC_API_KEY which is
unavailable on fork pull_request events. Gate only that step instead
of the entire job so steps 3-5 still validate omc update on all PRs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Installs Claude Code + old omc@4.9.3, runs omc update, then triggers
a Claude Code session to exercise session-start hooks. Any stderr or
non-zero exit fails CI.
Constraint: secrets.ANTHROPIC_API_KEY required — unavailable to fork PRs
Constraint: adopts Architect's simpler path (no stale cache pre-population)
Rejected: stale cache registration via installed_plugins.json | adds
complexity without proportional coverage; omc update exercises the upgrade
path without it
Confidence: high
Scope-risk: narrow
Directive: Do not add release tag triggers to this workflow
Not-tested: claude --print hook initialization on all Claude Code versions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The performance test "should have no performance impact when flags are not
set" was flaky in CI - it took 322ms vs the 100ms threshold due to
resource contention in the CI environment.
Constraint: CI runners have variable load; 100ms is too tight for a
deterministic pass.
Confidence: high
Scope-risk: narrow
The two slash commands in Quick Start were in a single code block,
causing users to copy-paste both at once which fails. Split into
separate blocks with a "Then:" separator and added a note that
they must be entered one at a time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The rebase onto origin/dev preserved the hook behavior, but the bridge-routing test still expected a missing ralplan state file even though current bridge logic seeds inert awaiting-confirmation state for natural-language invocations. This follow-up narrows the resolution to the failing expectation and regenerated dist artifacts so the rebased branch verifies cleanly before force-push.
Constraint: Rebase onto current origin/dev changed the observable ralplan startup contract without changing this PR's intended bridge implementation
Constraint: Keep the post-rebase fix limited to test expectations and generated artifacts
Rejected: Revert the inert ralplan state seeding in src/hooks/bridge.ts | would undo the intended #2623 behavior already covered elsewhere in the suite
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If ralplan keyword-routing startup semantics change again, update both source and dist bridge-routing expectations together
Tested: npm run test:run -- src/hooks/__tests__/bridge-routing.test.ts src/hooks/persistent-mode/__tests__/team-ralplan-stop.test.ts; npm run build; tsc --noEmit on src/hooks/__tests__/bridge-routing.test.ts via LSP diagnostics
Not-tested: Full repository test suite
If stalePath's parent directory was removed (deleted config tree),
symlinkSync throws ENOENT and the repair is silently skipped.
Create the parent directory recursively before creating the temp symlink.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
If renameSync fails after tmpLink creation (e.g., replacing an existing
junction on Windows), the fallback path recreates the symlink at stalePath.
Without unlinking first, symlinkSync throws EEXIST on a pre-existing
dangling link and the error is swallowed — leaving the stale root broken.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
POSIX symlink target was relative (just version name), resolving from
symlink parent — wrong when stalePath isn't under cacheBase.
Always use absolute path so the symlink works regardless of parent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)