* feat(release): weekly release train — channels, onboarding, dashboard banner, cron Implements the full release pipeline described in release-process.html (supersedes #1525, which only had the workflow scaffolding). A. Release infrastructure — .github/workflows/canary.yml triggers on a cron ('30 17 * * 5,6,0,1,2', i.e. 23:00 IST Fri–Tue) plus workflow_dispatch, without the stale-SHA guard or the merged-PR-comment step from #1525 (cron has no merged-PR context). release.yml uses changesets/action. .changeset/config.json adds the snapshot template and moves the private @aoagents/ao-web to ignore[]. B. Channel awareness (packages/cli/src/lib/update-check.ts) — new updateChannel field in the global-config Zod schema (stable | nightly | manual; defaults to manual so existing users see no surprise installs). fetchLatestVersion now reads dist-tags[channel] from the registry; isVersionOutdated compares prerelease segments numerically + lexically so SHA-suffixed nightlies sort correctly. maybeShowUpdateNotice and scheduleBackgroundRefresh skip entirely on manual. C. Active-session guard (packages/cli/src/commands/update.ts) — before any handle*Update proceeds, sm.list() filters for working/idle/ needs_input/stuck and refuses with `N session(s) active. Run `ao stop` first.` instead of auto-stopping (per the design doc: surprise-killing user work is worse than refusing). D. Soft auto-install + onboarding — handleNpmUpdate skips the confirm prompt on stable/nightly. New packages/cli/src/lib/update-channel- onboarding.ts prompts once on the first `ao start` after this lands; ask-once gate keyed on the absence of updateChannel in the global config; dismissal persists `manual`. New `ao config set updateChannel <value>` command (also handles installMethod). E. Dashboard banner — packages/web/src/app/api/version/route.ts reads the same cache file the CLI writes (~/.cache/ao/update-check.json, XDG-aware) and rejects cache entries from a different channel. packages/web/src/app/api/update/route.ts duplicates the active-session guard so the dashboard can return a structured 409. New UpdateBanner component wired into Dashboard.tsx — Tailwind only, var(--color-*) tokens, dismissible per-version via localStorage, deferred fetch so it doesn't shift the call order in existing dashboard tests. F. Bun + Homebrew detection (update-check.ts) — new classifiers for ~/.bun/install/global/ (auto-installs `bun add -g @aoagents/ao@<channel>`) and /Cellar/ao/ (notice-only — `brew upgrade ao`, never auto-install because brew owns the symlinks). New installMethod override field in the global config to pin detection when path heuristics fail. Tests: +155 (B/C/F unit, onboarding ask-once gate, /api/version + /api/update, UpdateBanner visibility/dismiss/click). pnpm test, pnpm typecheck, pnpm lint all green for the changes; the same 10 pre-existing test failures observed on main are still present (all environment-dependent: ~/.cache/ao state, codex binary install, /private path canonicalization on macOS). Closes #1525 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): CI failures + Greptile review feedback CI fixes: - Web /api/update spawn ENOENT — attach `child.on("error", ...)` so the asynchronous spawn-error event from a missing `ao` binary doesn't bubble up as an unhandled error and crash vitest. The route already returns 202 before the error fires; on real installs the user sees "no version change" if the install fails. - start.test.ts pollution — runStartup calls `maybePromptForUpdateChannel`, which (with isHumanCaller mocked to true) writes to the real ~/.agent-orchestrator/config.yaml on the CI runner via persistUpdateChannel. Subsequent tests then load that newly-created (empty-projects) config and report "No projects configured" instead of the expected "project not found". Fix: stub `update-channel-onboarding.js` in start.test.ts so runStartup is a no-op for the channel prompt. Review feedback: - (P1) `runtime: "tmux"` hardcoded default in `persistUpdateChannel` and `loadOrInit` would lock Windows users into a non-functional tmux config when they dismiss the channel prompt. Both now use `getDefaultRuntime()`, matching `makeEmptyGlobalConfig` in core's global-config.ts. - (P2) `hasChosenUpdateChannel` JSDoc inverted — the second "True when" bullet actually described the False case. Rewritten with separate True/False sections that match the implementation. - (P2) `isVersionOutdated` was duplicated between the CLI and the dashboard /api/version route. Moved to a new shared module `packages/core/src/version-compare.ts`, exported from `@aoagents/ao-core`, consumed by both CLI (re-exports as `isVersionOutdated`) and the web route directly. Added 14 unit tests in core for the canonical implementation. Defensive: `maybePromptForUpdateChannel` now validates the prompt result via `UpdateChannelSchema.safeParse` before persisting — never writes `undefined` or an unrecognized string to disk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): Windows spawn + dismiss-while-blocked review feedback - (P1) `ao update` silently never ran on Windows because `spawn("ao", ...)` doesn't consult PATHEXT, so npm's `ao.cmd` shim wasn't found and the async ENOENT was swallowed by the error handler. Add `shell: isWindows()` + `windowsHide: true` per the cross-platform guide. - (P1) Dismiss button was inert when the banner was in the `blocked` (409) or `error` phase — `setDismissedFor` set the localStorage flag but the hide condition required `phase === "idle"`, so the banner stayed pinned until reload. `handleDismiss` now resets phase to idle (and clears the error message) so the existing condition fires. Added a regression test covering dismiss from the 409 path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): runNpmInstall on Windows — shell:true so PATHEXT resolves npm.cmd (P1) The dashboard /api/update spawn got `shell: isWindows()` + `windowsHide: true` in 9f29131d, but `runNpmInstall` in the CLI's `ao update` command was still missing the same fix. On Windows, `spawn("npm", ...)` without a shell wrapper doesn't consult PATHEXT, so npm/pnpm/bun's `*.cmd` shims never resolve and the install silently ENOENTs. Mirror the fix into runNpmInstall — it's the single spawn site behind every non-git, non-homebrew install path (npm-global, pnpm-global, bun-global, unknown), so this one change covers all four install methods. Tests: - Mock `isWindows` from @aoagents/ao-core so the spawn options can be inspected per-platform. - Assert `shell: true, windowsHide: true, stdio: "inherit"` on Windows. - Assert `shell: false` on macOS / Linux. - Parametrize over pnpm-global / bun-global to confirm the same options flow through every npm-style install command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): /api/version reads cached.isOutdated for git installs (P1) The dashboard banner never appeared for git-installed users because `/api/version` ran `isVersionOutdated(current, "origin/main")`, and `parseVersion("origin/main")` produces NaN parts that the early-exit guard catches with `return false`. Git installs cache `latestVersion` as a git ref (not a semver) and a precomputed `isOutdated` flag from `git fetch + merge-base`; the CLI special-cases this in `update-check.ts`. Mirror the same pattern here: cached.installMethod === "git" ? cached.isOutdated === true : isVersionOutdated(current, latest) Also extend the local CacheData with `installMethod?: string` and `isOutdated?: boolean` so the new branch type-checks. Kept as `string` rather than importing the CLI's `InstallMethod` type — the literal "git" compare is the only thing that matters here, and the web package shouldn't take a dep on @aoagents/ao-cli. Two new tests cover the git-install path: one asserts isOutdated=true is trusted from the cache, the other asserts isOutdated=false (current with origin) is trusted too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): must-fix #3+#4 — global-config layout + git-only flag guard #3 — ensureNoActiveSessions now consults loadGlobalConfig() first as a quick "any projects registered?" check, then routes through loadConfig(globalPath) only when the registry actually has projects (loadConfig dispatches to buildEffectiveConfigFromGlobalConfigPath when given the canonical global path — see packages/core/src/config.ts). Defends against AO_GLOBAL_CONFIG override to a non-canonical path. Three new tests cover: registered-projects path fires the guard correctly; empty registry returns early without building a SessionManager; missing global file returns early without even reading it. #4 — Restored the rejection of git-only flags on non-git installs. Users copy/pasting `ao update --skip-smoke` from older docs would silently no-op on npm/pnpm/bun installs. Now exits non-zero with: "--skip-smoke only applies to git installs (current install: npm-global)." Test it.each across npm/pnpm/bun/homebrew/unknown plus a positive test that git installs still accept the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): #2 should-fix — channel-switch prompt When a stable user runs `ao config set updateChannel nightly` and then `ao update`, isVersionOutdated(0.5.0, 0.5.0-nightly-abc) returns false (per semver, prerelease < stable on equal base). The old code printed "Already on latest nightly" and exited without installing — confusing, because the install command we'd run is genuinely a different dist-tag. Fix: snapshot the previously-cached channel BEFORE forcing a refresh, then detect a switch via `previousChannel !== activeChannel && !info.isOutdated`. On switch: - Don't take the "already on latest" early-return. - Print a yellow "Channel switch detected: was X, now Y." notice. - Force a confirm prompt regardless of stable/nightly soft-install, defaulting to "no" (channel-switch should be explicit). Manual users still see their normal prompt. Onboarding copy now includes one line about channel switches: "switching later prompts before installing the other channel's build." 4 new tests: explicit switch fires the prompt + installs on yes; declines on no; same-channel doesn't fire (back to "Already on latest"); first-ever update with no previous cache doesn't fire either. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(release-train): polish — drop setTimeout, dedup defaults, share cache, dedup export #5 — UpdateBanner no longer wraps its mount fetch in setTimeout(0). Production code shouldn't bend to test mock ordering. Instead, the two brittle Dashboard tests that relied on `mockImplementationOnce` queue ordering now route by URL via `mockImplementation`, and the cadence test asserts "no other endpoints were touched" instead of "no fetch was touched at all". Also added a deliberate "no interval / re-fetch" comment per #6. #7 — Promoted core's `makeEmptyGlobalConfig` to the public `createDefaultGlobalConfig` (kept the internal alias for back-compat). Both the CLI's `persistUpdateChannel` and `loadOrInit` (in `ao config`) now call it instead of inlining the same defaults block. Single source of truth. #8 — New `packages/core/src/update-cache.ts` exports `getUpdateCheckCachePath`, `readUpdateCheckCacheRaw`, and `getInstalledAoVersion`. The CLI's `update-check.ts` keeps its richer install-method/channel/git-rev validation but now delegates path resolution and version lookup to core. The dashboard's `/api/version` route drops its duplicated `getCachePath`/`readCache`/`getCurrentVersion` and consumes from core directly. Cache layout is one file, not two. #9 — Removed the duplicate `export { isManualOnlyInstall }` from `update.ts` (also dropped the unused import). The canonical export lives in `update-check.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release-train): cosmetic — workflow rename note, design tokens, changeset trim #1 release.yml: added a comment above `workflows: [CI]` warning that GitHub matches by name (not filename) and silently no-ops on mismatch — so a rename of ci.yml's `name:` field would mean releases stop triggering. #10 UpdateBanner: replaced text-[13px] / text-[12px] with text-sm / text-xs to match the dashboard's chrome scale. #6 Banner refresh: noted in the existing useEffect comment that we don't re-fetch — re-evaluate if "user kept tab open for days, missed an update" becomes a real complaint. #11 .changeset/release-train.md: dropped @aoagents/ao-web from the version bump list. The package is `private: true` and in changeset's ignore[], so listing it was cosmetic and would just clutter the eventual release notes with a non-published artifact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): illegalcall review — global guard, channel scoping, publishable web (#1, PRRT_kwDORPZUAc6BHFDf) — `ensureNoActiveSessions` now ALWAYS loads from the canonical global config, never from project-local. The previous code preferred `loadConfig()` (local search-upward) when run inside a repo, which made `sm.list()` enumerate only that project's sessions — active work in other registered projects would be missed and the install would proceed. New regression test asserts that a session in `other-project` blocks the update even when invoked from `this-project`'s cwd. Existing global-config tests retained. (#2, PRRT_kwDORPZUAc6BHFOl) + (#4, PRRT_kwDORPZUAc6BHFon) — Reverted the `private: true` on @aoagents/ao-web. Because @aoagents/ao-cli has a workspace:* runtime dep on it (for `findWebDir()`/dashboard files), pnpm rewrites the dep on publish to a literal version — keeping ao-web private would make `npm install -g @aoagents/ao` fail. Restored ao-web to the changeset linked group, removed it from `ignore[]`, restored the release- train changeset entry, added publishConfig + repository metadata. New `scripts/check-publishable-deps.mjs` walks every package and asserts that no publishable package has a workspace:* runtime dep on a `private: true` package. Wired into both release.yml and canary.yml before the publish step so any future regression is caught at CI rather than at the user's `npm install`. Verified the script catches the inverse condition. (#3, PRRT_kwDORPZUAc6BHFaV) — `readCachedUpdateInfo` now treats a missing `data.channel` as a miss when an explicit channel is provided. Previous logic only rejected when both data.channel and channel were set AND differed, so a legacy cache entry (pre-channel-scoping) could keep returning stale stable state to a user who had since switched to nightly until the 24h TTL expired. Existing fixtures bumped to include `channel` where the test exercises the checkForUpdate / maybeShowUpdateNotice path; new regression test exercises the legacy-no-channel case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): SHA-suffix nightly compare — never miss a banner (P1, PRRT_kwDORPZUAc6BJLrW) Git SHAs are uniformly-random hex, so the old `comparePrereleaseSegments` lexical fallback gave the wrong answer ~50% of the time on snapshot tags. Concretely: user installs `0.5.0-nightly-f00d123`, CI publishes `0.5.0-nightly-0dead01`, and `'f' < '0'` returns false → banner never shows. Fix: when two prerelease segments are both non-numeric and differ, treat the left side as older (return -1). The cache layer always carries the registry's CURRENT dist-tag, so any non-numeric mismatch on the same base means the installed copy is behind by construction. Numeric ordering (`rc.1 < rc.2`) and numeric-vs-non-numeric (`0.5.0-1 < 0.5.0-alpha`) are unchanged. Tradeoff: a user who manually installed `0.5.0-beta` while the registry only publishes `0.5.0-alpha` would see a spurious banner. AO's release pipeline only emits SHA-suffixed nightly prereleases, so the scenario doesn't occur in practice — documented in the function's JSDoc. Updated two misleadingly-named tests ("orders SHA-suffixed nightlies lexically") that had been asserting the buggy behavior; new tests cover the specific case from the review (`nightly-f00d123` vs `nightly-0dead01`) and preserve the numeric-ordering invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(release-train): multi-project active-session proof + changeset note (#1, PRRT_kwDORPZUAc6BHFDf follow-up) Dhruv asked for proof — not a comment — that loadConfig(globalPath) actually enumerates across all registered projects, not just the cwd's. New test in update.test.ts seeds proj-a and proj-b in the global config, places one active session in each (one "working", one "needs_input"), and asserts the refusal stderr lists BOTH session ids AND the total count says "2 sessions active". The test is specifically named so it shows up in `vitest run -t "Dhruv proof"`. Verified `pnpm changeset version` locally — @aoagents/ao-web, ao-cli, and ao all bump to 0.7.0 together via the linked group, confirming the install-404 class of bug is gone. Also updated the release-train changeset to drop the stale "moves the private @aoagents/ao-web to ignore" line — that contradicts the current state (ao-web is publishable and in the linked group). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): Ashish P1+P2 — dashboard banner now actually installs P1 — Dashboard banner click was a no-op for npm users. POST /api/update spawns `ao update` with `stdio: "ignore"`, which makes `isTTY()` return false in the child. The old handleNpmUpdate hit the non-TTY branch ("Run: ...") and exited without installing. Banner returned 202 "started"; nothing actually happened. Fix (Ashish's option c, with an env-var bridge): - /api/update spawns with `AO_NON_INTERACTIVE_INSTALL=1` on the env. - handleNpmUpdate computes `interactive = isTTY() && !isApiInvoked()`. - Restructured so the early-return only fires for non-TTY + non-API (piped output): we still print "Run: ..." for that case, matching the old contract. API-invoked path now actually runs runNpmInstall, skipping the confirm prompt (would hang the detached child forever). Three new CLI tests: - AO_NON_INTERACTIVE_INSTALL=1 → spawn invoked even with isTTY=false. - The piped-output case (no env var, no TTY) still prints "Run: ...". - Active-session guard still fires in the API-invoked path (defense in depth — the route's own guard isn't single point of trust). P2 — First nightly opt-in stuck on "Already on latest stable". Repro: user on stable 0.5.0, runs `ao config set updateChannel nightly`, runs `ao update`. previousChannel was undefined, isOutdated was false (semver: prerelease < stable on equal base), so the early return fired and the install never ran. Fix: new `isFirstChannelOptIn` branch — `previousChannel === undefined && info.currentVersion !== info.latestVersion && !info.isOutdated`. Force the same prompt path the channel-switch case uses (default=no, explicit consent). Confirmed install path covered by a new test that mirrors the repro exactly. The pre-existing "no previous cache → no prompt" test asserted the OLD buggy behavior; rewritten to assert the canonical case (no prior cache AND versions match → still "Already on latest", no prompt). P2 — Dashboard /api/version legacy cache. Same class as Dhruv #3, this time on the web side. Old code: const cacheMatchesChannel = !cache?.channel || cache.channel === channel; A legacy entry without `channel` would short-circuit `!cache?.channel` and serve stale latestVersion. Fixed to require `cache.channel === channel` explicitly. New regression test seeds a no-channel entry and asserts { latest: null, isOutdated: false, checkedAt: null }. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release-train): Dhruv edge-case — running.json is the live source of truth (PRRT_kwDORPZUAc6BUIUK) The active-session guard previously short-circuited on empty global config, which missed the case where: - User runs `ao start` from a repo with a local agent-orchestrator.yaml and no global registration. - running.json lists that project as currently being polled. - Sessions live on disk under ~/.agent-orchestrator/{hash}-{projectId}/. In that state, `loadGlobalConfig().projects` is empty so the old early return fired and `ao update` would proceed while a daemon was actively supervising the user's in-flight work. Fix: consult `getRunning()` BEFORE falling back to the global registry. When running.json reports projects, trust its configPath (could be a local project yaml OR the canonical global path — `loadConfig` dispatches on shape) and build the SessionManager from there. The global fallback is now the no-daemon-running case, where on-disk sessions get reconciled by SessionManager enrichment. Three new tests in update.test.ts: - `refuses when sessions exist in a locally-registered project not in global config (Dhruv edge-case)` — seeds running.json with a local-only project + working session, asserts refusal + that loadConfig was called with running.configPath (NOT the global path). - `returns true (allows update) when running.json is gone and global is empty` — covers the genuinely-safe case. - `trusts running.json over an inconsistent global config` — when both signals exist, the live one (running.json) wins and loadGlobalConfig is never consulted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(release-train): shift canary cron 23:00 → 23:30 IST Schedule moves to 23:30 IST = 18:00 UTC. Cron expression changes from `30 17 * * 5,6,0,1,2` to `0 18 * * 5,6,0,1,2`. Same DOW window (Fri,Sat,Sun,Mon,Tue) so the bake window (Wed–Thu) is unaffected. Files touched (all consistent): - .github/workflows/canary.yml — cron expression + comment block - .changeset/release-train.md — schedule string in feature description - CONTRIBUTING.md — "Testing your changes" callout Verified `grep -rn '23:00 IST|17:30 UTC|"30 17'` returns zero matches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| CHANGELOG.md | ||
| README.md | ||
| package.json | ||
| tsconfig.json | ||
README.md
notifier-openclaw
OpenClaw notifier plugin for AO escalation events.
Quick setup
ao setup openclaw
This interactive wizard auto-detects your OpenClaw gateway, validates the connection, and writes the config. For non-interactive use (e.g., in CI/CD pipelines or automation scripts):
ao setup openclaw --url http://127.0.0.1:18789/hooks/agent --token YOUR_TOKEN --non-interactive
Required OpenClaw config (openclaw.json)
{
"hooks": {
"enabled": true,
"token": "<your-hooks-token>",
"allowRequestSessionKey": true,
"allowedSessionKeyPrefixes": ["hook:"]
}
}
AO config (agent-orchestrator.yaml)
notifiers:
openclaw:
plugin: openclaw
url: http://127.0.0.1:18789/hooks/agent
token: ${OPENCLAW_HOOKS_TOKEN}
Behavior
- Sends
POST /hooks/agentpayloads with per-session keyhook:ao:<sessionId>. - Defaults
wakeMode: nowanddeliver: true. - Retries on
429and5xxresponses with exponential backoff.
Token rotation
- Rotate
hooks.tokenin OpenClaw. - Update
OPENCLAW_HOOKS_TOKENused by AO. - Verify old token returns
401and new token returns200.
Known limitation (Phase 0)
- OpenClaw hook ingest is not idempotent by default. Replayed webhook payloads are processed as separate runs.
- Owner: AO integration.
- Follow-up: add stable event id/idempotency key support.