Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/backend-boundary-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
{"scope":"hostIdRule","target":"omarchy.weather","category":"omarchyBackend","owner":"hancore.shibumi.center","capability":"weather","count":6,"backend":"omarchy.weather","removalStep":"Step 8"},
{"scope":"hostIdRule","target":"omarchy.wifiqr","category":"omarchyBackend","owner":"hancore.shibumi.network","capability":"network","count":3,"backend":"omarchy.network","removalStep":"Step 5"},
{"scope":"commandRule","value":"omarchy-agent-usage-","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"},
{"scope":"commandRule","value":"omarchy-agent-usage-codex","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"},
{"scope":"commandRule","value":"omarchy-agent-usage-update","category":"omarchyBackend","owner":"hancore.shibumi.ai","capability":"ai-usage","count":1,"backend":"omarchy.agents","removalStep":"Step 8"},
{"scope":"commandRule","value":"omarchy-audio-output-set-default","category":"omarchyBackend","owner":"hancore.shibumi.bluetooth","capability":"bluetooth","count":2,"backend":"omarchy.audio","removalStep":"Step 4A"},
{"scope":"commandRule","value":"omarchy-audio-output-sink","category":"omarchyBackend","owner":"hancore.shibumi.audio","capability":"audio","count":1,"backend":"omarchy.audio","removalStep":"Step 4A"},
Expand Down Expand Up @@ -396,6 +397,7 @@
],
"commandRules": [
{"value": "omarchy-agent-usage-", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the host Agents collector command after the Shibumi provider adapter is authoritative."},
{"value": "omarchy-agent-usage-codex", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the revision-bound Codex collector compatibility probe with the complete Step 8 provider migration."},
{"value": "omarchy-agent-usage-update", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.ai", "capability": "ai-usage", "backend": "omarchy.agents", "removalStep": "Step 8", "reason": "Remove the host Agents update command after the Shibumi provider adapter is authoritative."},
{"value": "omarchy-audio-output-set-default", "count": 2, "category": "omarchyBackend", "owner": "hancore.shibumi.bluetooth", "capability": "bluetooth", "backend": "omarchy.audio", "removalStep": "Step 4A", "reason": "The transitional Bluetooth route and native Audio seam both use this helper until the atomic cutover removes the former."},
{"value": "omarchy-audio-input-set-default", "count": 1, "category": "omarchyBackend", "owner": "hancore.shibumi.audio", "capability": "audio", "backend": "omarchy.audio", "removalStep": "Step 4A", "reason": "Native Audio must preserve the official input-route handoff until the atomic cutover is complete."},
Expand Down
8 changes: 6 additions & 2 deletions hancore.shibumi.ai/AiUsagePanel.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ShibumiPanel {
readonly property int renderedModelCount: modelRepeater.count
readonly property bool providerReady: provider && provider.ready !== undefined
? provider.ready === true : true
readonly property string providerStatusLabel: provider && aiService
&& typeof aiService.providerStatusText === "function"
? aiService.providerStatusText(provider)
: providerReady ? "live" : "stale"
readonly property bool providerHasUsage: provider
&& (Number(provider.rateLimitPercent) >= 0
|| Number(provider.secondaryRateLimitPercent) >= 0)
Expand Down Expand Up @@ -223,8 +227,8 @@ ShibumiPanel {
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: panel.providerReady ? "live" : "stale"
color: panel.providerReady ? panel.controlMuted
text: panel.providerStatusLabel
color: panel.providerStatusLabel === "live" ? panel.controlMuted
: panel.controlAccent
font.family: panel.bar ? panel.bar.fontFamily : Commons.Style.font.family
font.pixelSize: Commons.Style.font.caption
Expand Down
12 changes: 11 additions & 1 deletion hancore.shibumi.ai/Service.qml
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@ Item {
return authHelp || "No current usage or limit data."
}

function providerStatusText(provider) {
if (!provider || provider.ready === false) return "stale"
if (String(provider.providerId || "") === "codex"
&& Number(provider.rateLimitPercent) < 0
&& Number(provider.secondaryRateLimitPercent) < 0)
return "partial"
return "live"
}

function displayPercent(provider, value) {
const number = Number(value)
if (!isFinite(number) || number < 0) return -1
Expand Down Expand Up @@ -431,7 +440,8 @@ Item {
let heading = String(provider.providerName || provider.providerId || "AI")
if (displayTierLabel(provider.tierLabel))
heading += " (" + displayTierLabel(provider.tierLabel) + ")"
if (provider.ready === false) heading += " · stale"
const status = providerStatusText(provider)
if (status !== "live") heading += " · " + status
lines.push(heading)
if (Number(provider.rateLimitPercent) >= 0) {
const reset = resetText(provider, provider.rateLimitResetAt)
Expand Down
36 changes: 35 additions & 1 deletion hancore.shibumi.ai/scripts/agents-update
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ set -euo pipefail
omarchy_path=${1:-}
shift || true
timeout_seconds=${SHIBUMI_AI_UPDATE_TIMEOUT:-120}
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
compat_bin="$script_dir/compat-bin"
child_pid=""
watchdog_pid=""
unset SHIBUMI_AI_REAL_CODEX

fail() {
printf 'shibumi-ai agents update: %s\n' "$*" >&2
Expand Down Expand Up @@ -88,6 +91,8 @@ if [[ ${1:-} == --probe-only ]]; then
exit 0
fi

codex_requested=false
agent_id_count=0
for argument in "$@"; do
case $argument in
--force | --limits-only) continue ;;
Expand All @@ -97,9 +102,38 @@ for argument in "$@"; do
|| fail "invalid agent id: $argument"
collector="$omarchy_path/bin/omarchy-agent-usage-$argument"
[[ -x $collector ]] || fail "agent collector is missing: $collector"
agent_id_count=$((agent_id_count + 1))
[[ $argument != codex ]] || codex_requested=true
done
if (( agent_id_count == 0 )) \
&& [[ -x $omarchy_path/bin/omarchy-agent-usage-codex ]]; then
codex_requested=true
fi

compat_environment=()
if $codex_requested; then
codex_search_path=${PATH:-/usr/bin:/bin}
for candidate_dir in \
"${HOME:?}/.local/bin" \
"$HOME/.npm-global/bin" \
"$HOME/.local/share/mise/shims"; do
case :$codex_search_path: in
*:"$candidate_dir":*) ;;
*) codex_search_path+=":$candidate_dir" ;;
esac
done
real_codex=$(PATH="$codex_search_path" type -P codex 2>/dev/null || true)
if [[ $real_codex == /* && -x $real_codex \
&& ! $real_codex -ef "$compat_bin/codex" ]]; then
compat_environment+=(
"PATH=$compat_bin:$codex_search_path"
"SHIBUMI_AI_REAL_CODEX=$real_codex"
)
fi
fi

setsid env OMARCHY_PATH="$omarchy_path" "$updater" "$@" &
setsid env OMARCHY_PATH="$omarchy_path" \
"${compat_environment[@]}" "$updater" "$@" &
child_pid=$!
watch_parent "$$" "$child_pid" &
watchdog_pid=$!
Expand Down
45 changes: 45 additions & 0 deletions hancore.shibumi.ai/scripts/compat-bin/codex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env bash

set -euo pipefail

real_codex=${SHIBUMI_AI_REAL_CODEX:-}
if [[ $real_codex != /* || ! -x $real_codex \
|| $real_codex -ef "${BASH_SOURCE[0]}" ]]; then
printf 'shibumi-ai codex compatibility: real Codex command is unavailable\n' >&2
exit 127
fi

arguments=("$@")
app_server=false
for argument in "${arguments[@]}"; do
if [[ $argument == app-server ]]; then
app_server=true
break
fi
done

# Omarchy v4.0.0 requests the former `untrusted` approval policy. Codex 0.149
# removed that spelling in favor of `never`, so its app-server exits before
# initialize and the otherwise authoritative host collector publishes no
# limits. Translate only this removed app-server argument; every other Codex
# invocation and argument passes through unchanged.
if $app_server; then
for ((index = 0; index < ${#arguments[@]}; index++)); do
case ${arguments[index]} in
-a|--ask-for-approval)
if (( index + 1 < ${#arguments[@]} )) \
&& [[ ${arguments[index + 1]} == untrusted ]]; then
arguments[index + 1]=never
fi
;;
-a=untrusted)
arguments[index]=-a=never
;;
--ask-for-approval=untrusted)
arguments[index]=--ask-for-approval=never
;;
esac
done
fi

exec "$real_codex" "${arguments[@]}"
48 changes: 47 additions & 1 deletion tests/ai-plugin-regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,51 @@ set -e
[[ $missing_collector_rc -ne 0 ]] \
|| fail "missing requested agent collector was accepted"

compat_root="$tmpdir/compat-omarchy"
compat_real_bin="$tmpdir/home/.local/bin"
compat_base_bin="$tmpdir/compat-base-bin"
compat_log="$tmpdir/state/codex-compat-args"
mkdir -p "$compat_root/bin" "$compat_real_bin" "$compat_base_bin"
for utility in bash dirname env setsid sleep; do
ln -s "$(command -v "$utility")" "$compat_base_bin/$utility"
done
cat >"$compat_root/bin/omarchy-agent-usage-update" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
env codex -a untrusted exec
env codex -s read-only -a untrusted app-server
EOF
install -m 0755 /usr/bin/true \
"$compat_root/bin/omarchy-agent-usage-codex"
cat >"$compat_real_bin/codex" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >>"${XDG_STATE_HOME:?}/codex-compat-args"
EOF
chmod 0755 "$compat_root/bin/omarchy-agent-usage-update" \
"$compat_real_bin/codex"
run_codex_compat_case() {
local label=$1
shift
: >"$compat_log"
PATH="$compat_base_bin" \
HOME="$tmpdir/home" XDG_STATE_HOME="$tmpdir/state" \
"$wrapper" "$compat_root" "$@" >/dev/null
[[ $(wc -l <"$compat_log") -eq 2 ]] \
|| fail "Codex compatibility shim changed the $label invocation count"
grep -Fx -- '-a untrusted exec' "$compat_log" >/dev/null \
|| fail "Codex compatibility shim changed a non-app-server $label invocation"
grep -Fx -- '-s read-only -a never app-server' "$compat_log" >/dev/null \
|| fail "Codex compatibility shim did not translate the $label policy"
}
# shellcheck disable=SC2329 # Exported to prove PATH-only lookup ignores it.
codex() { return 97; }
export -f codex
run_codex_compat_case explicit codex
run_codex_compat_case all-agents
run_codex_compat_case forced-all --force
unset -f codex

widget="$repo_root/hancore.shibumi.ai/BarWidget.qml"
service="$repo_root/hancore.shibumi.ai/Service.qml"
rg -q 'serviceFor\("hancore\.shibumi\.ai"\)' "$widget" \
Expand Down Expand Up @@ -312,7 +357,8 @@ rg -q 'newest_model\(messages, today_ms, tomorrow_ms\)' \
"$repo_root/hancore.shibumi.ai/scripts/opencode-usage" \
|| fail "OpenCode latest model does not share the today scope"
[[ -x $repo_root/hancore.shibumi.ai/scripts/opencode-usage \
&& -x $repo_root/hancore.shibumi.ai/scripts/agents-update ]] \
&& -x $repo_root/hancore.shibumi.ai/scripts/agents-update \
&& -x $repo_root/hancore.shibumi.ai/scripts/compat-bin/codex ]] \
|| fail "AI provider scripts are not executable"
[[ -s $repo_root/hancore.shibumi.ai/assets/codex.svg \
&& -s $repo_root/hancore.shibumi.ai/assets/opencode-mark.svg ]] \
Expand Down
92 changes: 92 additions & 0 deletions tests/ai-plugin-smoke.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ ShellRoot {
property bool waitingBackendTransition: false
property bool waitingExpiryProbe: false
property var expiryProbeRecord: null
property int codexStatusProbeStage: 0
property var codexStatusOriginalRecord: null
property string codexStatusOriginalTool: ""
property bool codexStatusProbePassed: false
property bool midnightRacePassed: false
property int midnightRaceStage: 0
property int midnightRaceTicks: 0
Expand Down Expand Up @@ -81,6 +85,7 @@ ShellRoot {
&& codex.todayTotalTokens === 1031649
&& codex.latestModel === ""
&& claude.ready && codex.ready
&& agentsService.providerStatusText(codex) === "live"
&& !agentsService.providerHasCurrentData(claude)
&& agentsService.providerCurrentDataMessage(claude)
=== "Run `claude auth login` to restore authoritative usage."
Expand All @@ -92,6 +97,86 @@ ShellRoot {
&& typeof codex.refresh !== "function"
}

function codexStatusRecord(ready, withLimits) {
return JSON.stringify({
schemaVersion: 1,
id: "codex",
name: "Codex",
ready: ready,
updatedAt: new Date().toISOString(),
limits: withLimits
? [{ label: "Weekly (7-day)", percent: 0.24, resetsAt: "" }] : [],
tierLabel: "",
usageStatusText: withLimits ? "" : "Codex limits unavailable",
authHelpText: withLimits ? "" : "initialize",
todayPrompts: 14,
todaySessions: 2,
todayTotalTokens: 1031649,
totalPrompts: 69089,
totalSessions: 136,
activeDays: 31,
modelUsage: { "gpt-test": { inputTokens: 700000 } }
})
}

function advanceCodexStatusProbe() {
if (codexStatusProbeStage === 0) {
codexStatusOriginalRecord = agentsService.agentsCodexRecord
codexStatusOriginalTool = fakeState.selectedTool
fakeState.selectedTool = "codex"
fakeState.revision++
agentsState.revision++
agentsService.applyAgentRecord("codex",
codexStatusRecord(true, false))
codexStatusProbeStage = 1
return false
}

const provider = agentsService.providerFor("codex")
if (!provider) {
fail("Codex status probe lost its schema-v1 record")
return false
}
if (codexStatusProbeStage === 1) {
if (agentsService.providerStatusText(provider) !== "partial"
|| emptyAgentsPanel.providerStatusLabel !== "partial"
|| agentsService.tooltipText().indexOf("Codex · partial") < 0) {
fail("Codex partial status did not propagate")
return false
}
agentsService.applyAgentRecord("codex",
codexStatusRecord(true, true))
codexStatusProbeStage = 2
return false
}
if (codexStatusProbeStage === 2) {
if (agentsService.providerStatusText(provider) !== "live"
|| emptyAgentsPanel.providerStatusLabel !== "live"
|| agentsService.tooltipText().indexOf("Codex · partial") >= 0) {
fail("Codex live status did not replace partial")
return false
}
agentsService.applyAgentRecord("codex",
codexStatusRecord(false, true))
codexStatusProbeStage = 3
return false
}
if (agentsService.providerStatusText(provider) !== "stale"
|| emptyAgentsPanel.providerStatusLabel !== "stale"
|| agentsService.tooltipText().indexOf("Codex · stale") < 0) {
fail("Codex stale status did not take precedence")
return false
}

agentsService.agentsCodexRecord = codexStatusOriginalRecord
agentsService.providerRevision++
fakeState.selectedTool = codexStatusOriginalTool
fakeState.revision++
agentsState.revision++
codexStatusProbePassed = true
return true
}

function readyRecordWithoutCurrentDataAccepted() {
const recordNow = Date.now()
const currentUpdatedAt = new Date(recordNow).toISOString()
Expand Down Expand Up @@ -899,6 +984,11 @@ ShellRoot {
if (first.panelLoaded || secondLoader.item !== null
|| root.clickTargets.length !== 1)
return root.fail("panel/widget teardown")
if (!root.codexStatusProbePassed) {
root.advanceCodexStatusProbe()
root.ticks = 0
return
}
if (!root.waitingExpiryProbe) {
if (!root.readyRecordWithoutCurrentDataAccepted())
return root.fail("ready empty/stale agents record contract")
Expand All @@ -925,6 +1015,8 @@ ShellRoot {
agentsService.providerRevision++
if (agentsService.providerFor("claude").ready
|| agentsService.providerFor("codex").ready
|| agentsService.providerStatusText(
agentsService.providerFor("codex")) !== "stale"
|| agentsService.usagePercent(
agentsService.providerFor("claude")) !== -1
|| agentsService.usagePercent(
Expand Down