Skip to content
Open
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
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
projection_provider: ${{ steps.filter.outputs.projection_provider }}
kafka_runtime: ${{ steps.filter.outputs.kafka_runtime }}
event_sourcing: ${{ steps.filter.outputs.event_sourcing }}
console_web: ${{ steps.filter.outputs.console_web }}
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -83,6 +84,9 @@ jobs:
- 'tools/ci/orleans_garnet_persistence_smoke.sh'
- 'tools/ci/architecture_guards.sh'
- '.github/workflows/ci.yml'
console_web:
- 'apps/aevatar-console-web/**'
- '.github/workflows/ci.yml'

fast-gates:
if: github.event_name != 'schedule' && (github.event_name == 'workflow_dispatch' || needs.changes.outputs.core_code == 'true')
Expand Down Expand Up @@ -110,6 +114,44 @@ jobs:
- name: Test Stability Guards
run: bash tools/ci/test_stability_guards.sh

console-web:
if: |
github.event_name != 'schedule' && (
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) ||
needs.changes.outputs.console_web == 'true'
)
needs: changes
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.2.1
run_install: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
cache-dependency-path: apps/aevatar-console-web/pnpm-lock.yaml

- name: Install console-web dependencies
run: pnpm --dir apps/aevatar-console-web install --frozen-lockfile

- name: Type-check console-web
run: pnpm --dir apps/aevatar-console-web tsc

- name: Test console-web
run: pnpm --dir apps/aevatar-console-web test --runInBand

- name: Build console-web
run: pnpm --dir apps/aevatar-console-web build

split-test-guards:
if: |
github.event_name == 'workflow_dispatch' ||
Expand Down
196 changes: 196 additions & 0 deletions WORKFLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
---
tracker:
kind: github
api_key: $GITHUB_TOKEN
project_slug: aevatarAI/aevatar
active_states:
- Todo
- In Progress
- Rework
terminal_states:
- Done
- Closed
- Cancelled
- Canceled
- Duplicate

polling:
interval_ms: 30000

workspace:
root: $SYMPHONY_WORKSPACE_ROOT

git:
user_name: eanzhao
email: yiqi.zhao@aelf.io

hooks:
after_create: |
gh auth setup-git --hostname github.com --force
gh repo clone aevatarAI/aevatar . -- --depth=1
before_run: |
set -euo pipefail
gh auth setup-git --hostname github.com --force
git fetch origin
DEFAULT_BRANCH="${SYMPHONY_DEFAULT_BRANCH:-dev}"
BRANCH_PATTERN="*_issue-${SYMPHONY_ISSUE_NUMBER}-symphony"
EXISTING_REMOTE="$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/${BRANCH_PATTERN}" | head -n1 || true)"
EXISTING_LOCAL="$(git for-each-ref --format='%(refname:short)' "refs/heads/${BRANCH_PATTERN}" | head -n1 || true)"
if [ -n "$EXISTING_REMOTE" ]; then
BRANCH="${EXISTING_REMOTE#origin/}"
git checkout "$BRANCH"
git pull origin "$BRANCH"
elif [ -n "$EXISTING_LOCAL" ]; then
BRANCH="$EXISTING_LOCAL"
git checkout "$BRANCH"
else
BRANCH="chore/$(date +%F)_issue-${SYMPHONY_ISSUE_NUMBER}-symphony"
git checkout "$DEFAULT_BRANCH"
git pull origin "$DEFAULT_BRANCH"
git checkout -b "$BRANCH" "origin/$DEFAULT_BRANCH"
fi
dotnet restore aevatar.slnx --nologo
after_run: |
echo "Symphony finished ${SYMPHONY_ISSUE_IDENTIFIER}"
timeout_ms: 1200000

agent:
default: codex
max_concurrent_agents: 1
max_turns: 20
max_retry_backoff_ms: 300000
auto_merge: false
require_label: symphony

agents:
codex:
command: codex app-server
approval_policy: never
thread_sandbox: workspace-write
network_access: true
turn_timeout_ms: 3600000
read_timeout_ms: 10000
stall_timeout_ms: 600000

server:
port: 8081
---

You are working on Aevatar issue {{ issue.identifier }}: {{ issue.title }}.

Read `AGENTS.md` and `CLAUDE.md` before making changes. If they conflict, follow `AGENTS.md`.

## Repository Context

- Default branch: `dev`
- Main solution: `aevatar.slnx`
- .NET SDK: `10.0.103`
- Main workflow host: `src/workflow/Aevatar.Workflow.Host.Api`
- Mainnet host: `src/Aevatar.Mainnet.Host.Api`
- Frontend app: `apps/aevatar-console-web` using `pnpm`
- Do not introduce or document services on ports `5000` or `5050`

## Highest-Priority Repo Rules

1. Keep strict layering: `Domain / Application / Infrastructure / Host`
2. Keep CQRS, projection, actor, and read-model boundaries intact
3. Do not add process-local runtime state registries or generic query/reply shortcuts
4. Use strong typing for stable business semantics; do not push clear semantics into generic bags
5. Delete dead code instead of preserving compatibility shells

## Issue

- Identifier: {{ issue.identifier }}
- State: {{ issue.state }}
- URL: {{ issue.url }}

{% if issue.description %}
{{ issue.description }}
{% endif %}

{% if attempt %}
This is continuation attempt {{ attempt }}. Resume from the current workspace state and do not redo completed work.
{% endif %}

## Required Execution Flow

1. If the issue has label `todo`, move it to `in-progress` before coding:
`gh issue edit {{ issue.identifier }} --repo aevatarAI/aevatar --remove-label todo --add-label in-progress`
2. Work on the branch already checked out by the hook:
`git branch --show-current`
Do not create a different branch name manually.
3. Stay inside the issue scope. Do not fix unrelated problems. If you find one, create a separate issue.
4. Use one persistent issue comment as a workpad with marker `## Symphony Workpad`.
5. After implementation and verification, push the current branch and create or update a PR.
6. When ready for handoff, move the issue to `human-review`:
- from `in-progress`: remove `in-progress`, add `human-review`
- from `rework`: remove `rework`, add `human-review`
7. If blocked, update the workpad with the blocker, leave a concise explanation, and move the issue to `human-review`.

## Workpad Commands

```bash
MARKER="## Symphony Workpad"
COMMENT_ID="$(gh api repos/aevatarAI/aevatar/issues/{{ issue.identifier | remove: "#" }}/comments --jq ".[] | select(.body | startswith(\"$MARKER\")) | .id" | head -n1)"
if [ -z "$COMMENT_ID" ]; then
gh issue comment {{ issue.identifier }} --repo aevatarAI/aevatar --body "$MARKER
- [ ] Understand issue
- [ ] Implement change
- [ ] Verify
- [ ] Prepare PR / handoff"
COMMENT_ID="$(gh api repos/aevatarAI/aevatar/issues/{{ issue.identifier | remove: "#" }}/comments --jq ".[] | select(.body | startswith(\"$MARKER\")) | .id" | head -n1)"
fi
```

Update the existing workpad comment instead of creating new progress comments:

```bash
gh api repos/aevatarAI/aevatar/issues/comments/$COMMENT_ID -X PATCH -f body="$MARKER
- [x] Understand issue
- [ ] Implement change
- [ ] Verify
- [ ] Prepare PR / handoff

Current focus: ..."
```

## Verification Expectations

Run the smallest relevant validation set that honestly covers your changes:

- Default backend validation: `dotnet build aevatar.slnx --nologo`
- Run targeted tests for touched projects, for example:
`dotnet test test/<RelevantProject>.Tests/<RelevantProject>.Tests.csproj --nologo`
- If you touch `apps/aevatar-console-web`, run:
`pnpm --dir apps/aevatar-console-web lint`
- If you change workflow, projection, query/read-model, or architecture-sensitive code, run the relevant guard scripts from `AGENTS.md`

Do not claim tests passed unless you actually ran them.

## PR Flow

Use the current checked-out branch for push and PR creation:

```bash
BRANCH="$(git branch --show-current)"
git push -u origin "$BRANCH"
PR="$(gh pr list --repo aevatarAI/aevatar --head "$BRANCH" --json number --jq '.[0].number')"
if [ -z "$PR" ]; then
gh pr create \
--repo aevatarAI/aevatar \
--base dev \
--head "$BRANCH" \
--title "{{ issue.identifier }}: {{ issue.title }}" \
--body "Closes {{ issue.identifier }}"
fi
```

## Finish Conditions

- Requested work is implemented
- Relevant validation ran
- Changes are committed and pushed
- PR exists or was updated
- Issue label moved to `human-review`

Once those are done, stop. Do not keep polishing beyond the issue scope.
3 changes: 3 additions & 0 deletions apps/aevatar-console-web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,11 @@ NYXID_BASE_URL=http://127.0.0.1:3001
NYXID_CLIENT_ID=your-public-client-id
NYXID_REDIRECT_URI=http://127.0.0.1:5173/auth/callback
NYXID_SCOPE="openid profile email"
# Optional when deploying under a sub-path such as /console/
AEVATAR_CONSOLE_PUBLIC_PATH=/
```

`NYXID_BASE_URL` and `NYXID_CLIENT_ID` are required. The console no longer ships a baked-in NyxID tenant or client id.
`NYXID_REDIRECT_URI` must exactly match the public client registration in NyxID.
If you change `.env.local`, restart `pnpm dev` so Umi reloads the injected env values.

Expand Down
18 changes: 17 additions & 1 deletion apps/aevatar-console-web/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ import routes from './routes';

const { UMI_ENV = 'dev' } = process.env;

function resolvePublicPath(value?: string): string {
const normalized = value?.trim();
if (!normalized || normalized === '/') {
return '/';
}

const rootRelative = normalized.startsWith('/') ? normalized : `/${normalized}`;
return rootRelative.endsWith('/') ? rootRelative : `${rootRelative}/`;
}

/**
* @name 使用公共路径
* @description 部署时的路径,如果部署在非根目录下,需要配置这个变量
* @doc https://umijs.org/docs/api/config#publicpath
*/
const PUBLIC_PATH: string = '/';
const PUBLIC_PATH: string = resolvePublicPath(
process.env.AEVATAR_CONSOLE_PUBLIC_PATH,
);

const config: ReturnType<typeof defineConfig> = defineConfig({
/**
Expand All @@ -23,6 +35,7 @@ const config: ReturnType<typeof defineConfig> = defineConfig({
*/
hash: true,

base: PUBLIC_PATH,
publicPath: PUBLIC_PATH,

/**
Expand Down Expand Up @@ -145,6 +158,9 @@ const config: ReturnType<typeof defineConfig> = defineConfig({
process.env.NYXID_REDIRECT_URI,
),
'process.env.NYXID_SCOPE': JSON.stringify(process.env.NYXID_SCOPE),
'process.env.AEVATAR_CONSOLE_PUBLIC_PATH': JSON.stringify(
process.env.AEVATAR_CONSOLE_PUBLIC_PATH,
),
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/aevatar-console-web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { QueryClientProvider } from "@tanstack/react-query";
import { Avatar, ConfigProvider, Dropdown, Space, Typography } from "antd";
import enUS from "antd/locale/en_US";
import React from "react";
import { history } from "@umijs/max";
import { history } from "@/shared/navigation/history";
import BrandLogo from "@/components/BrandLogo";
import defaultSettings from "../config/defaultSettings";
import { errorConfig } from "./requestErrorConfig";
Expand Down
2 changes: 1 addition & 1 deletion apps/aevatar-console-web/src/pages/actors/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ProTable,
} from "@ant-design/pro-components";
import { useQuery } from "@tanstack/react-query";
import { history } from "@umijs/max";
import { history } from "@/shared/navigation/history";
import {
Alert,
Button,
Expand Down
31 changes: 10 additions & 21 deletions apps/aevatar-console-web/src/pages/auth/callback/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { PageLoading } from '@ant-design/pro-components';
import { Button, Result } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useModel } from '@umijs/max';
import { NyxIDAuthClient } from '@/shared/auth/client';
import { getNyxIDRuntimeConfig } from '@/shared/auth/config';
import { loadStoredAuthSession } from '@/shared/auth/session';

const CallbackPage: React.FC = () => {
const { initialState, setInitialState } = useModel('@@initialState');
const [errorText, setErrorText] = useState<string | undefined>(undefined);
const config = useMemo(() => getNyxIDRuntimeConfig(), []);

useEffect(() => {
let cancelled = false;

if (loadStoredAuthSession()) {
window.location.replace('/overview');
return () => {
cancelled = true;
};
}

const finishLogin = async () => {
try {
const client = new NyxIDAuthClient(config);
Expand All @@ -21,19 +27,6 @@ const CallbackPage: React.FC = () => {
return;
}

setInitialState((current) =>
current
? {
...current,
auth: {
enabled: true,
isAuthenticated: true,
config,
session: result.session,
},
}
: current,
);
window.location.replace(result.returnTo);
} catch (error) {
if (cancelled) {
Expand All @@ -44,16 +37,12 @@ const CallbackPage: React.FC = () => {
}
};

if (!initialState?.auth?.isAuthenticated) {
void finishLogin();
} else {
window.location.replace('/overview');
}
void finishLogin();

return () => {
cancelled = true;
};
}, [config, initialState?.auth?.isAuthenticated, setInitialState]);
}, [config]);

if (errorText) {
return (
Expand Down
Loading
Loading