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
41 changes: 34 additions & 7 deletions .cursor/skills/release-publish/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,12 @@ Once approved, execute in order:

1. **Ensure clean state**: `git status` should show no uncommitted changes.
Switch to `main` if not already there.
2. **Run tests**: `npm test` — abort if any fail.
3. **Verify the OpenClaw dependency**: if AlphaClaw depends on a pinned
2. **Preflight stable template access**: for a stable release, run the Render
repository permission and remote checks in Phase 4.5 before changing the
AlphaClaw version. Abort if the authenticated user cannot push to Render's
template; all deployment templates must remain in sync with the release.
3. **Run tests**: `npm test` — abort if any fail.
4. **Verify the OpenClaw dependency**: if AlphaClaw depends on a pinned
`openclaw` version, confirm `package-lock.json` and the local install resolve
to the same version before publishing.
```
Expand All @@ -115,10 +119,10 @@ Once approved, execute in order:
```
grep -R -n "allowConversationAccess" node_modules/openclaw/dist/zod-schema-* node_modules/openclaw/dist/runtime-schema-*
```
4. **Bump version**: `npm version <version>` (creates commit + tag).
5. **Push**: `git push && git push --tags`.
6. **Publish to npm**: `npm publish` (publishes to `latest` tag).
7. **Create GitHub release**:
5. **Bump version**: `npm version <version>` (creates commit + tag).
6. **Push**: `git push && git push --tags`.
7. **Publish to npm**: `npm publish` (publishes to `latest` tag).
8. **Create GitHub release**:
```
gh release create v<version> --title "AlphaClaw <version>" --notes "<body>"
```
Expand All @@ -136,7 +140,8 @@ Repos:
- `~/Projects/openclaw-railway-template` (typically `main` for production; `beta`
only when cutting a beta — see release-beta skill; merge `main` into `beta`
after stable pins when you want `beta` to match production pins)
- `~/Projects/openclaw-render-template` (typically `main`)
- `~/Projects/openclaw-render-template` (typically `main`; this checkout must
track `https://github.com/render-examples/openclaw-render-template.git`)
- `~/Projects/openclaw-apex-template` (typically `main`)

For **each** repo:
Expand All @@ -159,6 +164,28 @@ For **each** repo:
5. Commit and push (include both `package.json` and `package-lock.json` when the
lockfile exists or was added).

Before updating the Render template, verify that the authenticated GitHub user
can push to Render's repository and that the local checkout targets it:

```
gh api repos/render-examples/openclaw-render-template --jq '.permissions.push'
git -C ~/Projects/openclaw-render-template remote get-url origin
```

The permission check must return `true`, and `origin` must be
`https://github.com/render-examples/openclaw-render-template.git` (or the SSH
equivalent). If the checkout still targets the former `chrysb` repository,
update it before syncing:

```
git -C ~/Projects/openclaw-render-template remote set-url origin https://github.com/render-examples/openclaw-render-template.git
git -C ~/Projects/openclaw-render-template fetch origin
```

Stop before changing or publishing any template if push access is missing; ask
an administrator of `render-examples/openclaw-render-template` to grant the
authenticated user write access, then repeat the preflight.

Do not skip Render or Apex: pinning only one template while others stay on
`latest` causes drift and non-reproducible installs between platforms.

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Use this release flow when promoting tested beta builds to production:
- `npm version 0.3.2`
- `git push && git push --tags`
- `npm publish` (publishes to `latest`)
- Pin all deployment templates on `main` to that release: set `@chrysb/alphaclaw` in `~/Projects/openclaw-railway-template`, `~/Projects/openclaw-render-template`, and `~/Projects/openclaw-apex-template` to the released version (templates rely on AlphaClaw’s declared `openclaw` dependency — do not add `package.json` `overrides` for `openclaw` unless you have a one-off debug reason). Run `npm install` in each repo, confirm `npm ls openclaw` matches AlphaClaw’s `package.json` pin, commit `package.json` and `package-lock.json`, and push. Skipping a template leaves it stale relative to the others.
- Pin all deployment templates on `main` to that release: set `@chrysb/alphaclaw` in `~/Projects/openclaw-railway-template`, `~/Projects/openclaw-render-template`, and `~/Projects/openclaw-apex-template` to the released version. The Render checkout must track `render-examples/openclaw-render-template`; verify `gh api repos/render-examples/openclaw-render-template --jq '.permissions.push'` returns `true` before publishing, and stop if write access is missing. Templates rely on AlphaClaw’s declared `openclaw` dependency — do not add `package.json` `overrides` for `openclaw` unless you have a one-off debug reason. Run `npm install` in each repo, confirm `npm ls openclaw` matches AlphaClaw’s `package.json` pin, commit `package.json` and `package-lock.json`, and push. Skipping a template leaves it stale relative to the others.
5. Return templates to production channel:
- `@chrysb/alphaclaw: "latest"`
6. Optionally keep beta branch/tag flows active for next release cycle.
Expand Down
8 changes: 6 additions & 2 deletions lib/public/js/components/onboarding/welcome-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,16 @@ const getAiGroupError = (vals, ctx = {}) => {
if (!hasValue(vals.MODEL_KEY) || !String(vals.MODEL_KEY).includes("/")) {
return "Choose a model to continue.";
}
if (ctx.selectedProvider === "openai-codex" && ctx.codexLoading) {
if (
ctx.selectedProvider === "openai-codex" &&
ctx.codexLoading &&
!ctx.hasAi
) {
return "Checking Codex OAuth status. Try Next again in a moment.";
}
if (!ctx.hasAi) {
return ctx.selectedProvider === "openai-codex"
? "Connect Codex OAuth to continue."
? "Connect Codex OAuth or enter an OpenAI API key to continue."
: "Add credentials for the selected model provider to continue.";
}
return "";
Expand Down
62 changes: 58 additions & 4 deletions lib/public/js/components/onboarding/welcome-setup-step.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { h } from "preact";
import { useEffect, useState } from "preact/hooks";
import htm from "htm";
import { LoadingSpinner } from "../loading-spinner.js";
import { fetchOnboardProgress } from "../../lib/api.js";

const html = htm.bind(h);
const kSetupTips = [
Expand Down Expand Up @@ -30,9 +31,24 @@ const kSetupTips = [
text: "Be incredibly careful installing skills from the internet - they may contain malicious code.",
},
];
const kDefaultProgress = {
stage: "creating_repo",
message: "Creating repo...",
};
const kProgressStepNumbers = {
creating_repo: 1,
running_openclaw_onboard: 2,
initial_git_push: 3,
starting_gateway: 4,
};
const kProgressPollIntervalMs = 500;
const kProgressDotsIntervalMs = 450;
const kProgressDotSlots = [1, 2, 3];

export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
const [tipIndex, setTipIndex] = useState(0);
const [progress, setProgress] = useState(kDefaultProgress);
const [progressDotCount, setProgressDotCount] = useState(1);

useEffect(() => {
if (error || !loading) return;
Expand All @@ -42,6 +58,35 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
return () => clearInterval(timer);
}, [error, loading]);

useEffect(() => {
if (error || !loading) return;
let cancelled = false;
const refreshProgress = async () => {
try {
const progress = await fetchOnboardProgress();
if (!cancelled && progress?.message) {
setProgress(progress);
}
} catch {}
};
setProgress(kDefaultProgress);
refreshProgress();
const timer = setInterval(refreshProgress, kProgressPollIntervalMs);
return () => {
cancelled = true;
clearInterval(timer);
};
}, [error, loading]);

useEffect(() => {
if (error || !loading) return;
setProgressDotCount(1);
const timer = setInterval(() => {
setProgressDotCount((count) => (count % 3) + 1);
}, kProgressDotsIntervalMs);
return () => clearInterval(timer);
}, [error, loading]);

if (error) {
return html`
<div class="py-4 flex flex-col items-center text-center gap-3">
Expand Down Expand Up @@ -77,17 +122,26 @@ export const WelcomeSetupStep = ({ error, loading, onRetry, onBack }) => {
}

const currentTip = kSetupTips[tipIndex];
const progressStep = kProgressStepNumbers[progress.stage] || 1;
const progressLabel = (progress.message || kDefaultProgress.message).replace(/\.{1,3}$/, "");

return html`
<div class="relative min-h-[320px] pt-4 pb-20 flex">
<div
class="flex-1 flex flex-col items-center justify-center text-center gap-4"
>
<${LoadingSpinner} className="h-8 w-8 text-body" />
<h3 class="text-lg font-semibold text-body">
Initializing OpenClaw...
</h3>
<p class="text-sm text-fg-muted">This could take 10-15 seconds</p>
<h3 class="text-lg font-semibold text-body">Initializing AlphaClaw...</h3>
<p class="text-sm text-fg-muted">
${progressStep} / 4: ${progressLabel}<span class="inline-flex" aria-hidden="true"
>${kProgressDotSlots.map(
(slot) => html`<span style=${{ visibility: slot <= progressDotCount ? "visible" : "hidden" }}
>.</span
>`,
)}</span
>
</p>
<p class="text-xs text-fg-muted">This could take up to 30 seconds</p>
</div>
<div
class="absolute bottom-3 left-3 right-3 bg-field border border-border rounded-lg px-3 py-2 text-xs text-fg-muted"
Expand Down
23 changes: 15 additions & 8 deletions lib/public/js/components/welcome/use-welcome.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import {
import { useCachedFetch } from "../../hooks/use-cached-fetch.js";
import { usePolling } from "../../hooks/usePolling.js";
import {
getModelProvider,
getAuthProviderFromModelProvider,
getFeaturedModels,
getOnboardingModelProvider,
getOnboardingModels,
isDeprecatedOnboardingModelKey,
getVisibleAiFieldKeys,
kProviderAuthFields,
} from "../../lib/model-config.js";
Expand Down Expand Up @@ -167,7 +169,7 @@ export const useWelcome = ({ onComplete }) => {
};

const applyModelCatalog = useCallback((payload) => {
const list = getModelCatalogModels(payload);
const list = getOnboardingModels(getModelCatalogModels(payload));
if (!payload) return;
const isRefreshing = isModelCatalogRefreshing(payload);
const isFallbackRefresh =
Expand All @@ -181,11 +183,14 @@ export const useWelcome = ({ onComplete }) => {
: null
: "No models found",
);
const currentModelIsDeprecated = isDeprecatedOnboardingModelKey(
vals.MODEL_KEY,
);
const defaultModelKey = getInitialOnboardingModelKey({
catalog: list,
currentModelKey: vals.MODEL_KEY,
currentModelKey: currentModelIsDeprecated ? "" : vals.MODEL_KEY,
});
if (!vals.MODEL_KEY && defaultModelKey) {
if ((!vals.MODEL_KEY || currentModelIsDeprecated) && defaultModelKey) {
setVals((prev) => ({ ...prev, MODEL_KEY: defaultModelKey }));
}
}, [setVals, vals.MODEL_KEY]);
Expand All @@ -212,16 +217,18 @@ export const useWelcome = ({ onComplete }) => {
}, [modelsFetchState.error]);

const getValidationContext = (currentVals = {}) => {
const currentSelectedProvider = getModelProvider(
String(currentVals.MODEL_KEY || "").trim(),
);
const currentSelectedProvider = getOnboardingModelProvider({
modelKey: currentVals.MODEL_KEY,
models,
});
const currentSelectedAuthProvider =
getAuthProviderFromModelProvider(currentSelectedProvider);
const currentProviderAuthFields =
kProviderAuthFields[currentSelectedAuthProvider] || [];
const currentHasAi =
currentSelectedProvider === "openai-codex"
? !!codexStatus.connected
? !!codexStatus.connected ||
!!String(currentVals.OPENAI_API_KEY || "").trim()
: currentProviderAuthFields.some((field) =>
!!String(currentVals[field.key] || "").trim(),
);
Expand Down
5 changes: 5 additions & 0 deletions lib/public/js/lib/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,11 @@ export async function fetchOnboardStatus() {
return res.json();
}

export async function fetchOnboardProgress() {
const res = await authFetch("/api/onboard/progress");
return res.json();
}

export async function runOnboard(vars, modelKey, { importMode = false } = {}) {
const res = await authFetch("/api/onboard", {
method: "POST",
Expand Down
38 changes: 30 additions & 8 deletions lib/public/js/lib/model-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ export const kFeaturedModelDefs = [
label: "Sonnet 4.6",
preferredKeys: ["anthropic/claude-sonnet-4-6"],
},
{
label: "Codex 5.3",
preferredKeys: [
"openai/gpt-5.3-codex",
"openai-codex/gpt-5.3-codex",
],
},
{
label: "GPT-5.5",
preferredKeys: ["openai/gpt-5.5", "openai-codex/gpt-5.5"],
Expand All @@ -42,6 +35,36 @@ export const kFeaturedModelDefs = [
},
];

const kDeprecatedOnboardingModelKeys = new Set([
"openai/gpt-5.3-codex",
"openai-codex/gpt-5.3-codex",
]);
const kCanonicalCodexOauthModelKeys = new Set([
"openai/gpt-5.4-mini",
"openai/gpt-5.5",
]);

export const isDeprecatedOnboardingModelKey = (modelKey) =>
kDeprecatedOnboardingModelKeys.has(String(modelKey || "").trim());

export const getOnboardingModels = (models = []) =>
models.filter(
(model) => !isDeprecatedOnboardingModelKey(model?.key),
);

export const getOnboardingModelProvider = ({ modelKey, models = [] } = {}) => {
const normalizedKey = String(modelKey || "").trim();
const model = models.find((candidate) => candidate?.key === normalizedKey);
if (
getModelProvider(normalizedKey) === "openai-codex" ||
kCanonicalCodexOauthModelKeys.has(normalizedKey) ||
model?.agentRuntime?.id === "codex"
) {
return "openai-codex";
}
return getModelProvider(normalizedKey);
};

export const kAlwaysAvailableModelDefs = [
{
key: "openai/gpt-5.4-mini",
Expand Down Expand Up @@ -345,7 +368,6 @@ export const kFeatureDefs = [
];

export const getVisibleAiFieldKeys = (provider) => {
if (provider === "openai-codex") return new Set();
const authProvider = getAuthProviderFromModelProvider(provider);
const fields = kProviderAuthFields[authProvider] || [];
return new Set(fields.map((field) => field.key));
Expand Down
11 changes: 11 additions & 0 deletions lib/server/onboarding/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,14 @@ const createOnboardingService = ({
vars,
modelKey,
importMode = false,
onProgress,
}) => {
const reportProgress = (stage) => {
if (typeof onProgress !== "function") return;
try {
onProgress(stage);
} catch {}
};
const validation = validateOnboardingInput({
vars,
modelKey,
Expand Down Expand Up @@ -425,6 +432,7 @@ const createOnboardingService = ({
syncApiKeyAuthProfilesFromEnvVars(authProfiles, varsToSave);

const [, repoName] = repoUrl.split("/");
reportProgress("creating_repo");
const repoCheck = await ensureGithubRepoAccessible({
repoUrl,
repoName,
Expand Down Expand Up @@ -481,6 +489,7 @@ const createOnboardingService = ({
);
}

reportProgress("running_openclaw_onboard");
if (!existingConfigPresent) {
const onboardArgs = buildOnboardArgs({
varMap,
Expand Down Expand Up @@ -551,6 +560,7 @@ const createOnboardingService = ({

ensureGatewayProxyConfig(getBaseUrl(req));

reportProgress("initial_git_push");
try {
const commitMsg = importMode
? "imported existing setup via AlphaClaw"
Expand All @@ -567,6 +577,7 @@ const createOnboardingService = ({
console.error("[onboard] Git push error:", e.message);
}

reportProgress("starting_gateway");
runOnboardedBootSequence();
return { status: 200, body: { ok: true } };
};
Expand Down
Loading
Loading