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
4 changes: 2 additions & 2 deletions .claude/commands/triage-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Skip silently when:

Classify as `needs-human` and exit when:

- Any changed file path is under `packages/web/` — submodule two-PR rule (AGENTS.md). The agent must never push a parent commit referencing an unmerged submodule SHA.
- Any changed file path is under `packages/owletto/` — submodule two-PR rule (AGENTS.md). The agent must never push a parent commit referencing an unmerged submodule SHA.
- Any changed file path is under `.github/workflows/` or is `scripts/setup-dev.sh` — infra blast radius.
- Any changed file path is `.github/triage-config.yml` or `.claude/commands/triage-pr.md` — these define triage policy itself. A PR modifying them must not be evaluated by the (potentially-modified) policy on its own branch; the agent escalates so a human can review the change against `main`.
- Any review comment contains case-insensitive: `security`, `credential`, `token`, `secret`, `auth bypass`, `P0`, or `P1`.
Expand Down Expand Up @@ -201,6 +201,6 @@ The marker line at the top is parsed by future runs to short-circuit on matching
- **Never split unnecessarily.** Do not propose splitting a PR whose title scope is consistent and whose size is under the 1000-line gate, even if it touches multiple files.
- **`.js` import suffix in TS sources.** When fixing imports, add `.js` extensions to relative imports (NodeNext resolution).
- **Typecheck drift.** Always run BOTH `make build-packages` (package-local tsc emit) and `bun run typecheck` (root tsc check) — they catch different things.
- **Submodule two-PR rule.** Any change under `packages/web/` → `needs-human`, full stop.
- **Submodule two-PR rule.** Any change under `packages/owletto/` → `needs-human`, full stop.
- **Unused parameters.** Delete them; never prefix with `_`.
- **Bun, not npm.** Hooks enforce this.
12 changes: 6 additions & 6 deletions .github/actions/setup-submodule/action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: "Set up private submodule"
description: >
Checks out packages/web via deploy key when available. For forks and
Checks out packages/owletto via deploy key when available. For forks and
runs without the secret, writes a stub package.json so bun workspaces still
resolve (the web assets just won't build).

Expand Down Expand Up @@ -30,27 +30,27 @@ runs:
run: |
set -euo pipefail
git config --global url."git@github.com:lobu-ai/owletto.git".insteadOf "https://github.com/lobu-ai/owletto.git"
git submodule update --init --recursive packages/web
git submodule update --init --recursive packages/owletto

- name: Write stub package.json when submodule is unavailable
id: stub
if: inputs.deploy-key == ''
shell: bash
run: |
set -euo pipefail
echo "No deploy key — writing stub package.json for packages/web"
mkdir -p packages/web
echo "No deploy key — writing stub package.json for packages/owletto"
mkdir -p packages/owletto
# The stub omits the real submodule's deps, which means
# `bun install --frozen-lockfile` will fail against bun.lock.
# Callers should branch on the `stubbed` output to relax checks
# that require the real submodule (frozen-lockfile, vite build,
# `git status --porcelain` after install).
printf '%s\n' \
'{' \
' "name": "@lobu/web",' \
' "name": "@lobu/owletto",' \
' "private": true,' \
' "version": "1.6.0",' \
' "description": "Stub — private submodule not initialized"' \
'}' \
> packages/web/package.json
> packages/owletto/package.json
echo "stubbed=true" >> "$GITHUB_OUTPUT"
2 changes: 1 addition & 1 deletion .github/triage-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ infra_paths:

# Submodule two-PR rule. Any change under these is needs-human.
two_pr_paths:
- packages/web/
- packages/owletto/

# Comment substring matches (case-insensitive) that escalate to needs-human.
escalation_keywords:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ jobs:
# directly via the workspace-installed binary so this works on
# whatever submodule SHA the parent repo points at.
if: steps.submodule.outputs.stubbed != 'true'
run: cd packages/web && ../../node_modules/.bin/vitest run
run: cd packages/owletto && ../../node_modules/.bin/vitest run

# Backend integration tests need a real Postgres + pgvector. We run them
# under Node (not bun) for two reasons: (1) the vitest suite uses Node-only
Expand Down Expand Up @@ -422,7 +422,7 @@ jobs:
exit 0
fi
changed=$(git diff --name-only "$BASE_SHA"...HEAD)
if echo "$changed" | grep -E '^(packages/web($|/)|\.github/workflows/mac-release\.yml$|\.github/actions/setup-submodule(/|$)|\.gitmodules$)' >/dev/null; then
if echo "$changed" | grep -E '^(packages/owletto($|/)|\.github/workflows/mac-release\.yml$|\.github/actions/setup-submodule(/|$)|\.gitmodules$)' >/dev/null; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
Expand All @@ -431,7 +431,7 @@ jobs:

# Cheap end-to-end Xcode build for the Mac app. `mac-release.yml` is
# workflow_dispatch only, so a rename or path move inside the owletto
# submodule (Mac source lives at `packages/web/apps/mac/`) can silently
# submodule (Mac source lives at `packages/owletto/apps/mac/`) can silently
# break the release pipeline and only surface the next time someone
# triggers a release. Build-only (no archive, no signing, no notarize)
# is enough to prove the Xcode project + submodule layout still compose.
Expand Down Expand Up @@ -471,6 +471,6 @@ jobs:
if: steps.submodule.outputs.stubbed != 'true'
run: |
xcodebuild \
-project packages/web/apps/mac/Lobu.xcodeproj \
-project packages/owletto/apps/mac/Lobu.xcodeproj \
-scheme Lobu -configuration Release build \
CODE_SIGNING_ALLOWED=NO
24 changes: 12 additions & 12 deletions .github/workflows/mac-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: mac-release
# auto-update.
#
# The Mac source lives in the private `lobu-ai/owletto` repo, mounted here as
# the `packages/web` submodule. Releases ship from this (public) repo so the
# the `packages/owletto` submodule. Releases ship from this (public) repo so the
# DMG download URL and Sparkle appcast URL stay reachable to anonymous users —
# private-repo Releases would 404 without auth.
#
Expand All @@ -31,7 +31,7 @@ name: mac-release
#
# No provisioning profile is required — the app ships only standard
# entitlements (the HealthKit entitlement is disabled; see
# packages/web/apps/mac/Lobu/Lobu.entitlements in the owletto submodule).
# packages/owletto/apps/mac/Lobu/Lobu.entitlements in the owletto submodule).
#
# Sparkle auto-updates: `SPARKLE_ED_PRIVATE_KEY` (64-byte base64 of seed||pub)
# is required. The job signs Lobu.dmg with `sign_update` and pushes a new
Expand Down Expand Up @@ -86,17 +86,17 @@ jobs:
echo "::error::Submodule was stubbed — OWLETTO_WEB_DEPLOY_KEY is missing or invalid. Aborting release."
exit 1
fi
remote_url=$(git -C packages/web config --get remote.origin.url)
remote_url=$(git -C packages/owletto config --get remote.origin.url)
case "$remote_url" in
*lobu-ai/owletto.git|*lobu-ai/owletto) ;;
*)
echo "::error::packages/web origin is '$remote_url' — expected lobu-ai/owletto. Aborting release."
echo "::error::packages/owletto origin is '$remote_url' — expected lobu-ai/owletto. Aborting release."
exit 1
;;
esac
for required in \
packages/web/apps/mac/Lobu.xcodeproj \
packages/web/apps/mac/Lobu/Info.plist \
packages/owletto/apps/mac/Lobu.xcodeproj \
packages/owletto/apps/mac/Lobu/Info.plist \
scripts/sparkle/update-appcast.py
do
if [ ! -e "$required" ]; then
Expand All @@ -114,7 +114,7 @@ jobs:
# `-list` text output has both `Targets:` and `Schemes:` sections,
# so a `grep '^ Lobu$'` could match a target named 'Lobu' even
# after the scheme was renamed to 'Owletto'.
SCHEMES=$(xcodebuild -project packages/web/apps/mac/Lobu.xcodeproj -list -json 2>/dev/null \
SCHEMES=$(xcodebuild -project packages/owletto/apps/mac/Lobu.xcodeproj -list -json 2>/dev/null \
| python3 -c "import json,sys; print('\n'.join(json.load(sys.stdin)['project']['schemes']))")
if ! echo "$SCHEMES" | grep -qE '^Lobu$'; then
echo "::error::Expected scheme 'Lobu' missing from project schemes (got: $SCHEMES)"
Expand All @@ -128,7 +128,7 @@ jobs:
EXPECTED_BUNDLE_NAME="Owletto"
for key in CFBundleName CFBundleDisplayName; do
actual=$(/usr/libexec/PlistBuddy -c "Print :$key" \
packages/web/apps/mac/Lobu/Info.plist 2>/dev/null || true)
packages/owletto/apps/mac/Lobu/Info.plist 2>/dev/null || true)
if [ "$actual" != "$EXPECTED_BUNDLE_NAME" ]; then
echo "::error::Info.plist:$key is '$actual', expected '$EXPECTED_BUNDLE_NAME'"
exit 1
Expand All @@ -149,7 +149,7 @@ jobs:
# correct.
EXPECTED_BUNDLE_ID="ai.lobu.mac"
distinct_ids=$(grep -E 'PRODUCT_BUNDLE_IDENTIFIER = ' \
packages/web/apps/mac/Lobu.xcodeproj/project.pbxproj \
packages/owletto/apps/mac/Lobu.xcodeproj/project.pbxproj \
| sed -E 's/.*PRODUCT_BUNDLE_IDENTIFIER = ([^;]+);.*/\1/' \
| sort -u)
if [ "$distinct_ids" != "$EXPECTED_BUNDLE_ID" ]; then
Expand All @@ -165,7 +165,7 @@ jobs:
# Use PlistBuddy so the check matches an actual plist key, not
# text inside an XML comment.
if ! /usr/libexec/PlistBuddy -c "Print :SUPublicEDKey" \
packages/web/apps/mac/Lobu/Info.plist >/dev/null 2>&1; then
packages/owletto/apps/mac/Lobu/Info.plist >/dev/null 2>&1; then
echo "::error::SUPublicEDKey missing from Info.plist — installed apps cannot verify updates"
exit 1
fi
Expand Down Expand Up @@ -209,7 +209,7 @@ jobs:
if: steps.mode.outputs.signed == 'true'
run: |
xcodebuild \
-project packages/web/apps/mac/Lobu.xcodeproj -scheme Lobu -configuration Release \
-project packages/owletto/apps/mac/Lobu.xcodeproj -scheme Lobu -configuration Release \
-archivePath "$RUNNER_TEMP/Lobu.xcarchive" archive \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Developer ID Application" \
Expand All @@ -221,7 +221,7 @@ jobs:
if: steps.mode.outputs.signed != 'true'
run: |
xcodebuild \
-project packages/web/apps/mac/Lobu.xcodeproj -scheme Lobu -configuration Release \
-project packages/owletto/apps/mac/Lobu.xcodeproj -scheme Lobu -configuration Release \
-archivePath "$RUNNER_TEMP/Lobu.xcarchive" archive \
CODE_SIGNING_ALLOWED=NO \
MARKETING_VERSION="${{ steps.v.outputs.version }}"
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,17 @@ jobs:
- name: Build packages
run: bun run build

# Frontend (web) ships with the product but isn't covered by
# Frontend (owletto) ships with the product but isn't covered by
# any other PR check: root `bun run build` skips it, and the root
# tsconfig excludes packages/web. Without this gate, a TS error
# tsconfig excludes packages/owletto. Without this gate, a TS error
# in the submodule (or a stale submodule pointer) can silently regress
# the deploy.
# Build connector-sdk first because the web submodule imports its compiled dist.
- name: Build frontend (web)
# Build connector-sdk first because the owletto submodule imports its compiled dist.
- name: Build frontend (owletto)
if: steps.submodule.outputs.stubbed != 'true'
run: |
cd packages/connector-sdk && bun run build && cd ../..
cd packages/web && bun run build
cd packages/owletto && bun run build

- name: Verify no uncommitted changes
if: steps.submodule.outputs.stubbed != 'true'
Expand Down
26 changes: 13 additions & 13 deletions .github/workflows/submodule-drift.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Submodule Drift

# Surfaces when packages/web (private) has merged commits past the
# Surfaces when packages/owletto (private) has merged commits past the
# parent's pinned SHA — i.e. the two-PR rule's parent bump is missing.
# Bot tag-update commits (FluxCD image automation) are exempted only when both
# the author and exact subject match — a sloppy human commit cannot slip past.
Expand Down Expand Up @@ -47,10 +47,10 @@ jobs:
# actions/checkout@v4 with default fetch-depth=1 only has the merge
# commit; pull the two endpoints by SHA so ls-tree can resolve them.
git fetch --depth=1 origin "$BASE_SHA" "$HEAD_SHA"
base_pointer=$(git ls-tree "$BASE_SHA" packages/web | awk '{print $3}')
head_pointer=$(git ls-tree "$HEAD_SHA" packages/web | awk '{print $3}')
base_pointer=$(git ls-tree "$BASE_SHA" packages/owletto | awk '{print $3}')
head_pointer=$(git ls-tree "$HEAD_SHA" packages/owletto | awk '{print $3}')
if [ "$base_pointer" != "$head_pointer" ]; then
echo "::error::Fork PRs cannot change the packages/web pointer."
echo "::error::Fork PRs cannot change the packages/owletto pointer."
echo " base: $base_pointer"
echo " head: $head_pointer"
echo "Submodule bumps must come from a same-repo PR (deploy-key-authorized)."
Expand All @@ -72,16 +72,16 @@ jobs:
shell: bash
run: |
set -euo pipefail
PINNED=$(git -C packages/web rev-parse HEAD)
git -C packages/web fetch --quiet origin main
REMOTE=$(git -C packages/web rev-parse origin/main)
PINNED=$(git -C packages/owletto rev-parse HEAD)
git -C packages/owletto fetch --quiet origin main
REMOTE=$(git -C packages/owletto rev-parse origin/main)

echo "Pinned (parent): $PINNED"
echo "owletto/main: $REMOTE"

# Hard rule: parent must never pin a SHA that isn't on owletto/main.
# FluxCD reads charts from owletto/main; an off-main pin breaks deploy.
if ! git -C packages/web merge-base --is-ancestor "$PINNED" origin/main; then
if ! git -C packages/owletto merge-base --is-ancestor "$PINNED" origin/main; then
echo "::error::Pinned SHA $PINNED is not reachable from owletto/main."
echo "This violates the rule against pinning unmerged submodule SHAs (see AGENTS.md)."
exit 1
Expand All @@ -95,7 +95,7 @@ jobs:
# Capture the log first so set -e propagates a git failure (process
# substitution would swallow it). tformat: guarantees a trailing
# newline so `read` doesn't drop the last commit.
LOG=$(git -C packages/web log \
LOG=$(git -C packages/owletto log \
--pretty='tformat:%h|%ae|%s' "$PINNED..origin/main")

# Walk each commit; exempt only those whose author email AND exact
Expand All @@ -106,7 +106,7 @@ jobs:
while IFS='|' read -r sha email subject; do
[ -z "$sha" ] && continue
if [ "$email" = "$BOT_AUTHOR_EMAIL" ] && [ "$subject" = "$BOT_SUBJECT" ]; then
outside=$(git -C packages/web show --name-only \
outside=$(git -C packages/owletto show --name-only \
--pretty='format:' "$sha" \
| sed '/^$/d' \
| grep -v '^deploy/' || true)
Expand All @@ -132,8 +132,8 @@ jobs:
printf '%s' "$DRIFT"
echo ""
echo "Fix (open as a separate PR):"
echo " git -C packages/web fetch origin"
echo " git -C packages/web checkout origin/main"
echo " git add packages/web"
echo " git -C packages/owletto fetch origin"
echo " git -C packages/owletto checkout origin/main"
echo " git add packages/owletto"
echo " git commit -m 'chore(submodule): bump owletto to current main'"
exit 1
4 changes: 2 additions & 2 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "packages/web"]
path = packages/web
[submodule "packages/owletto"]
path = packages/owletto
url = https://github.com/lobu-ai/owletto.git
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
- When fixing unused-parameter errors, delete the parameter rather than prefixing with `_`.

### Submodules
`packages/web` is a submodule of `lobu-ai/owletto`. Push the submodule change to a reachable branch first (usually `main`), then bump the pointer in the parent — the parent must never point at an unreachable SHA, or production cloning will fail.
`packages/owletto` is a submodule of `lobu-ai/owletto`. Push the submodule change to a reachable branch first (usually `main`), then bump the pointer in the parent — the parent must never point at an unreachable SHA, or production cloning will fail.

### Frontend (web)
When editing UI under `packages/web`, follow the design rules in @packages/web/DESIGN_GUIDELINES.md — confirmations, surfaces, empty states, selection, forms, page copy, radius, Sheet vs Dialog. Match the existing components and exemplar files referenced there; do not introduce new primitives without updating the guideline in the same PR.
### Frontend (owletto)
When editing UI under `packages/owletto`, follow the design rules in @packages/owletto/DESIGN_GUIDELINES.md — confirmations, surfaces, empty states, selection, forms, page copy, radius, Sheet vs Dialog. Match the existing components and exemplar files referenced there; do not introduce new primitives without updating the guideline in the same PR.

### Architecture

Expand Down
20 changes: 10 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ help:
@echo " make test-e2e - Boot the dev server + run openclaw-plugin e2e against it"
@echo " make eval - Run agent evals"
@echo " make clean-workers - Stop any running embedded worker subprocesses"
@echo " make typecheck - Strict typecheck (same as Dockerfile) for server + web"
@echo " make typecheck - Strict typecheck (same as Dockerfile) for server + owletto"

# Strict typecheck — mirrors the Dockerfile so local matches CI. Catches
# what `build-packages` (relaxed, bundler-only) misses.
typecheck:
@echo "🔎 Strict typecheck: packages/server..."
@( cd packages/server && bunx tsc --noEmit ) || exit $$?
@if [ -d packages/web/src ]; then \
echo "🔎 Strict typecheck: packages/web..."; \
( cd packages/web && bunx tsc -b --noEmit ) || exit $$?; \
@if [ -d packages/owletto/src ]; then \
echo "🔎 Strict typecheck: packages/owletto..."; \
( cd packages/owletto && bunx tsc -b --noEmit ) || exit $$?; \
fi
@echo "✅ Typecheck clean."

Expand All @@ -40,16 +40,16 @@ build-packages:
@( cd packages/cli && bun run build ) || exit $$?
@echo "✅ All packages built successfully!"

# Ensure packages/web is initialized; warn on drift but don't auto-fix
# Ensure packages/owletto is initialized; warn on drift but don't auto-fix
# (drift may be active feature-branch work — clobbering it silently is worse than the warning).
ensure-submodule:
@status=$$(git submodule status packages/web 2>/dev/null || true); \
@status=$$(git submodule status packages/owletto 2>/dev/null || true); \
case "$$status" in \
'-'*) echo ">> web submodule not initialized — running git submodule update --init --recursive"; \
git submodule update --init --recursive packages/web ;; \
'+'*) echo ">> WARNING: packages/web is at a different SHA than the parent pin:"; \
'-'*) echo ">> owletto submodule not initialized — running git submodule update --init --recursive"; \
git submodule update --init --recursive packages/owletto ;; \
'+'*) echo ">> WARNING: packages/owletto is at a different SHA than the parent pin:"; \
echo " $$status"; \
echo " If this is unintentional, run: git submodule update packages/web" ;; \
echo " If this is unintentional, run: git submodule update packages/owletto" ;; \
*) ;; \
esac

Expand Down
Loading
Loading