Install
openclaw skills install @cgartlab/argusUse when reviewing frontend code for design quality — checking design token usage, hardcoded values, dark mode coverage, accessibility compliance, CSS consistency, semantic HTML, or framework API usage. Use when auditing a component, page, or design system for issues. Trigger phrases: '帮我 review 这段代
openclaw skills install @cgartlab/argusWhen this skill is active, every line of frontend code is audited against the same standards: design tokens used correctly, no hardcoded values, dark mode fully covered, accessibility baseline met, correct API usage per technology stack, and copy-ready code fixes provided for every issue.
Argus can be invoked by the men agent team (cgartlab/men) as an optional review capability. This does not change standalone behavior — Argus runs identically with or without men, in any agent framework or as the argus-flash GitHub App.
The standard triggers above already cover review requests. Additional men-specific phrases:
When invoked from a men context (via --men-context flag or MEN_CONTEXT=1 env), Argus MAY frame its report for men's four-part summary template. See docs/men-integration.md.
[P#] file:line prefix is preserved verbatim — men parsers rely on it.| Men template slot | Argus output section |
|---|---|
| conclusion | Summary header (totals, stack, documentation) |
| key issues | Issue blocks grouped by severity |
| evidence | Found / Expected snippets + Reference links |
| open questions | Unresolved items (see Confidence rule) |
If the technology stack or the target file is not clearly identifiable, do not guess. List the item as unresolved in the open-questions section instead of flagging it with a fabricated severity or stack. This mirrors the men team's "clarify before acting" rule: unclear context is reported, never assumed.
Before reviewing, detect the project's technology stack:
| Indicator | Stack | Documentation |
|---|---|---|
*.tsx, *.jsx + react in package.json | React | https://react.dev, https://reactjs.org/docs |
*.vue | Vue | https://vuejs.org/guide |
*.svelte | Svelte | https://svelte.dev/docs |
angular.json | Angular | https://angular.dev/api |
*.astro | Astro | https://docs.astro.build |
lit-*.js / *.ts with lit imports | Lit | https://lit.dev/docs |
uno.config.ts / unocss.config.ts | UnoCSS | https://uno.antfu.me/ |
tailwind.config.js / tailwind.config.ts | Tailwind CSS | https://tailwindcss.com/docs |
| CSS/SCSS files only | Vanilla CSS | https://developer.mozilla.org/docs/Web/CSS |
Detection workflow:
package.json to confirm framework and versionuno.config.*, tailwind.config.*) — if present, review design token references inside utility classes (e.g. bg-*, px-*, text-*)Rule: Every color in component rules must be a var(--ds-*) reference. No bare oklch(), #hex, or rgb().
/* WRONG — bare oklch in component rule */
.ds-card {
background: oklch(99% 0.005 80);
color: oklch(20% 0.02 60);
}
/* RIGHT — token reference */
.ds-card {
background: var(--ds-color-surface);
color: var(--ds-color-fg);
}
Exception: Token declarations in :root and @keyframes may use bare oklch/hex.
Flag: Any occurrence of bare color value in component rules (CSS or inline style=).
Rule: All spacing, radii, and type scale values must use design token scale. No magic numbers.
/* WRONG */
padding: 16px;
border-radius: 8px;
/* RIGHT */
padding: var(--ds-space-4);
border-radius: var(--ds-radius-lg);
Flag: Any numeric value (not 0) that should be a design token but isn't.
Rule: Every color token declared in :root must have a [data-theme="dark"] override.
/* WRONG — no dark override */
:root {
--ds-color-bg: oklch(97% 0.012 80);
}
/* RIGHT — override exists */
[data-theme="dark"] {
--ds-color-bg: oklch(15% 0.008 75);
}
Flag: Any :root color token without a [data-theme="dark"] override. This is a silent dark mode break — colors may become unreadable.
Rule: WCAG AA baseline. Mandatory, never demoted to warning.
| Pattern | Requirement |
|---|---|
Icon-only <button> | aria-label present |
<img> | alt attribute present |
<a> without href | Not used as a button; use <button> |
| Focusable elements | Visible focus indicator |
| Color contrast | 4.5:1 for normal text, 3:1 for large text |
Flag: Any violation is P1 minimum.
catch {} blocks.parent a--active)id duplicates<a> tags without href used as interactive elements<button> for actions, <a> for links)Rule: Use framework APIs correctly per official documentation. See Framework Anti-Patterns Library below for specific patterns per framework.
React:
useState, useEffect, useCallback, useMemo deps arraysuseEffect cleanup functions present when neededReact.createClass, UNSAFE_ lifecycles)forwardRef, memo usage patterns/* WRONG — missing deps array */
useEffect(() => {
fetchData(id);
}, []);
/* RIGHT — deps array matches */
useEffect(() => {
fetchData(id);
}, [id]);
Vue:
ref vs reactive usagewatch vs watchEffect proper usageonMounted, not mounted)Angular:
ngOnInit, ngOnDestroy)Svelte:
$: reactivity declarations$store)onMount cleanupAstro:
client:*)Props interface.astro vs .jsx component boundariesGeneral JavaScript/TypeScript:
async/await error handlingComprehensive pattern catalog for each framework with detection rules, examples, and fixes.
Detection: useEffect( followed by variable without it in deps array
Reference: https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
// WRONG
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, []); // Missing: firstName, lastName
// RIGHT
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
Detection: useEffect( with async keyword before arrow function
Reference: https://react.dev/reference/react/useEffect#fetching-data-with-effects
// WRONG — useEffect cannot return a promise
useEffect(async () => {
const data = await fetchUser(id);
setUser(data);
}, [id]);
// RIGHT — use IIFE or separate function
useEffect(() => {
const fetchUser = async () => {
const data = await fetch(`/api/users/${id}`);
setUser(data);
};
fetchUser();
}, [id]);
Detection: JSX attribute with inline {} object or [] array
Reference: https://react.dev/learn/keeping-components-pure
// WRONG — new object/array on every render
<div style={{ color: 'red' }} />
<Child items={['a', 'b']} />
// RIGHT — move outside component or use useMemo
const buttonStyle = { color: 'red' };
const items = ['a', 'b'];
<div style={buttonStyle} />
<Child items={items} />
Detection: .map() without key prop on returned element
Reference: https://react.dev/learn/rendering-lists#keeping-list-items-in-order-with-key
// WRONG — missing key
users.map(user => <UserCard name={user.name} />)
// RIGHT — use stable unique id
users.map(user => <UserCard key={user.id} name={user.name} />)
Detection: Function referencing state/props without proper dependency Reference: https://react.dev/learn/avoiding-re-renders
// WRONG — count is stale
const handleClick = () => {
setCount(count + 1); // May use stale value
};
// RIGHT — use functional update
const handleClick = () => {
setCount(prev => prev + 1);
};
Detection: Component passing new object/function as prop without memoization Reference: https://react.dev/reference/react/memo
// WRONG — new function every render
const Parent = () => {
return <Child onClick={() => console.log(clicked)} />;
};
// RIGHT — memoize callback
const Parent = () => {
const handleClick = useCallback(() => {
console.log(clicked);
}, [clicked]);
return <Child onClick={handleClick} />;
};
Detection: Event listener or subscription without return cleanup Reference: https://react.dev/reference/react/useEffect#subscribing-to-events
// WRONG — memory leak
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []); // Missing cleanup
// RIGHT — cleanup function
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
Detection: State initialized with true/false when null/undefined is valid
Reference: https://react.dev/reference/react/useState
// WRONG — three states needed
const [isLoading, setIsLoading] = useState(true);
if (isLoading === true) // loading
else if (isLoading === false) // loaded
// But how to handle error?
// RIGHT — use proper state machine
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
Detection: useState storing value that can be computed from props/state
Reference: https://react.dev/learn/queueing-a-series-of-state-updates
// WRONG — redundant state
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// RIGHT — compute when needed
const fullName = `${firstName} ${lastName}`;
Detection: Multiple components passing same prop through layers Reference: https://react.dev/learn/passing-data-deeply-with-context
// WRONG — theme passed through layers
<GrandParent>
<Parent theme={theme}>
<Child theme={theme}>
<Button theme={theme} />
</Child>
</Parent>
</GrandParent>
// RIGHT — use context
const ThemeContext = createContext();
<ThemeContext.Provider value={theme}>
<Child />
</ThemeContext.Provider>
// Then useContext(ThemeContext) in Button
Detection: props: definition with mutation inside component
Reference: https://vuejs.org/guide/components/props#prop-mutations
// WRONG — mutating prop
<script setup>
const props = defineProps<{ title: string }>();
props.title = 'New Title'; // Error!
</script>
// RIGHT — emit event or use local state
<script setup>
const props = defineProps<{ title: string }>();
const localTitle = ref(props.title);
localTitle.value = 'New Title';
</script>
Detection: setup() function alongside data(), methods, computed
Reference: https://vuejs.org/guide/extras/composition-api-faq#should-i-use-options-api-or-composition-api
// WRONG — mixing APIs
<script>
export default {
data() { return { count: 0 } },
setup() {
const doubled = computed(() => this.count * 2); // Confusing
}
}
</script>
// RIGHT — stick to Composition API
<script setup>
const count = ref(0);
const doubled = computed(() => count.value * 2);
</script>
Detection: watch(obj, ...) instead of watch(() => obj.prop, ...)
Reference: https://vuejs.org/guide/essentials/watchers#watching-reactive-state
// WRONG — watches entire object
watch(user, (newUser) => {
console.log(newUser.name); // Won't trigger on name change
});
// RIGHT — watch specific property
watch(() => user.name, (newName) => {
console.log(newName);
});
Detection: const { prop } = reactive({...}) destructuring before use
Reference: https://vuejs.org/guide/essentials/reactivity-fundamentals#limitations-of-reactive
// WRONG — destructuring reactive() drops reactivity
<script setup>
const state = reactive({ name: 'John' });
const { name } = state; // name is a plain string, no longer reactive
function update() {
name.value = 'Jane'; // Does NOT update state.name
}
</script>
// RIGHT — prefer ref() (the default recommendation)
<script setup>
const name = ref('John');
function update() {
name.value = 'Jane'; // Reactivity preserved
}
</script>
// Alternative RIGHT — keep reactivity while destructuring
<script setup>
const state = reactive({ name: 'John' });
const { name } = toRefs(state);
function update() {
name.value = 'Jane'; // Updates state.name
}
</script>
Detection: computed() with mutation, async, or side effect
Reference: https://vuejs.org/guide/essentials/computed#computed-properties
// WRONG — side effect in computed
const fullName = computed(() => {
fetchUser(); // Side effect!
return `${user.firstName} ${user.lastName}`;
});
// RIGHT — use watch or method instead
const fullName = computed(() => `${user.firstName} ${user.lastName}`);
Detection: Subscription/timer in onMounted without onUnmounted
Reference: https://vuejs.org/guide/essentials/lifecycle#lifecycle-diagram
// WRONG
onMounted(() => {
interval = setInterval(fetchData, 5000);
}); // No cleanup!
// RIGHT
onMounted(() => {
interval = setInterval(fetchData, 5000);
});
onUnmounted(() => clearInterval(interval));
Detection: :key="index" in v-for
Reference: https://vuejs.org/guide/essentials/list#maintaining-state-with-key
// WRONG — key changes when array order changes
<div v-for="(item, index) in items" :key="index">
// RIGHT — use stable unique id
<div v-for="item in items" :key="item.id">
Detection: Direct prop assignment (props.title = ...) or manual update: emit when defineModel() applies
Reference: https://vuejs.org/guide/components/v-model
// WRONG — direct prop assignment breaks one-way data flow
<script setup>
const props = defineProps<{ title: string }>();
function rename() {
props.title = 'New'; // Warning: mutating a prop
}
</script>
// RIGHT — defineModel (Vue 3.4+)
<script setup>
const title = defineModel<string>();
function rename() {
title.value = 'New'; // Two-way binding via v-model
}
</script>
// Alternative RIGHT — explicit update:title emit
<script setup>
const props = defineProps<{ title: string }>();
const emit = defineEmits<{ 'update:title': [value: string] }>();
function rename() {
emit('update:title', 'New');
}
</script>
Detection: $store usage without understanding subscription lifecycle
Reference: https://svelte.dev/docs/svelte-store#auto-subscription
// WRONG — memory leak
<script>
import { count } from './stores';
onMount(() => {
// Using $count but not understanding subscription
});
</script>
// RIGHT — Svelte auto-subscribes with $ prefix
<script>
import { count } from './stores';
// $count is automatically subscribed and unsubscribed
</script>
<p>{$count}</p>
Detection: Multiple $: that could be combined into one
Reference: https://svelte.dev/docs/svelte/legacy-reactive-assignments
// WRONG — too many reactive statements
$: doubled = count * 2;
$: quadrupled = doubled * 2;
$: console.log(quadrupled);
// RIGHT — compute once
$: quadrupled = count * 4;
// Svelte 5 — prefer runes ($derived) over legacy $: statements
let quadrupled = $derived(count * 4);
Detection: export let followed by reassignment
Reference: https://svelte.dev/docs/svelte-components#script
// WRONG
<script>
export let name;
$: name = name.toUpperCase(); // Error!
</script>
// RIGHT — create derived value
<script>
export let name;
$: displayName = name?.toUpperCase();
</script>
<p>{displayName}</p>
Detection: Subscription or timer without onDestroy cleanup
Reference: https://svelte.dev/docs/svelte#ondestroy
// WRONG
<script>
import { onMount } from 'svelte';
let timer;
onMount(() => {
timer = setInterval(() => count++, 1000);
}); // Memory leak!
</script>
// RIGHT
<script>
import { onMount, onDestroy } from 'svelte';
let timer;
onMount(() => {
timer = setInterval(() => count++, 1000);
});
onDestroy(() => clearInterval(timer));
</script>
Detection: $store = direct assignment that bypasses a writable store's update()
Reference: https://svelte.dev/docs/svelte/stores#writable-stores
// WRONG — direct assignment bypasses the store's update() logic
$count = $count + 1;
// RIGHT — route changes through the store's update method
count.update(n => n + 1);
Detection: .subscribe() without .unsubscribe() or takeUntilDestroyed
Reference: https://angular.dev/api/core/rxjs-interop/takeUntilDestroyed
// WRONG — memory leak
@Component({...})
export class UserComponent {
ngOnInit() {
this.userService.getUser().subscribe(user => {
this.user = user;
});
}
}
// RIGHT — use takeUntilDestroyed (Angular 16+)
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@Component({...})
export class UserComponent {
// takeUntilDestroyed() inside an injection context works without `this.destroy$`
user$ = this.userService.getUser().pipe(takeUntilDestroyed());
}
// BEST — use async pipe in template
@Component({...})
export class UserComponent {
user$ = this.userService.getUser();
}
Detection: FormGroup initialized at field declaration, then overwritten with setValue() in ngOnInit
Reference: https://angular.dev/guide/forms/reactive-forms
// WRONG — setValue() in ngOnInit overwrites a field-initialized form
@Component({...})
export class ProfileComponent implements OnInit {
form = new FormGroup({
name: new FormControl(''),
});
ngOnInit() {
// Redundant for sync defaults; clobbers user input if async data arrives late
this.form.setValue({ name: 'John' });
}
}
// RIGHT — declare defaults once; patchValue() only when async data actually arrives
@Component({...})
export class ProfileComponent {
form = new FormGroup({
name: new FormControl(''),
});
constructor() {
this.userService.getProfile()
.pipe(takeUntilDestroyed())
.subscribe(profile => this.form.patchValue(profile)); // patch, not setValue
}
}
Detection: *ngIf="false" followed by display: none or [hidden]
Reference: https://angular.dev/api/common/NgIf
<!-- WRONG — double handling -->
<div *ngIf="show" [hidden]="!show" class="content">
Content
</div>
<!-- RIGHT — choose one -->
<div *ngIf="show" class="content">
Content
</div>
Detection: *ngFor without trackBy function
Reference: https://angular.dev/api/common/NgFor
<!-- WRONG — expensive re-renders -->
<div *ngFor="let item of items">
{{ item.name }}
</div>
<!-- RIGHT -->
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
trackById(index: number, item: Item): string {
return item.id;
}
Detection: HTTP call in constructor instead of ngOnInit Reference: https://angular.dev/guide/di
// WRONG — too early, may not have all dependencies
constructor(private http: HttpClient) {
this.http.get('/api/user').subscribe();
}
// RIGHT — wait for component initialization
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('/api/user').subscribe();
}
Detection: fetch() inside a client-side <script> (runtime request) when the data could be fetched in frontmatter at build time
Reference: https://docs.astro.build/en/recipes/build-time-data-fetching
// WRONG — fetch in client-side <script>: runs in the browser on every visit, no build caching
---
---
<script>
const data = await fetch('https://api.example.com/data').then(r => r.json());
</script>
// RIGHT — fetch in frontmatter: executes once at build time, page ships with data
---
const data = await fetch('https://api.example.com/data').then(r => r.json());
---
<p>{data.title}</p>
// Note: add a client:* directive only for interactivity — never for data fetching
<Interactive client:load />
Detection: Missing or incorrect Props interface
Reference: https://docs.astro.build/en/guides/typescript#component-props
// WRONG — no typing
---
const { title, count } = Astro.props;
// No TypeScript validation
---
// RIGHT — proper interface
---
interface Props {
title: string;
count?: number;
}
const { title, count = 0 } = Astro.props as Props;
---
Detection: Using .astro component in client script without directive
Reference: https://docs.astro.build/en/concepts/islands
// WRONG — client component without directive
import ReactButton from './ReactButton.jsx';
// RIGHT — use client directive
import ReactButton from './ReactButton.jsx';
<ReactButton client:load />
Detection: client:* on static components
Reference: https://docs.astro.build/en/reference/directives-reference#client-directives
// WRONG — static component doesn't need client directive
<StaticHeader client:load />
// RIGHT — only when interactivity needed
<InteractiveButton client:visible />
Detection: No TypeScript interface for component props Reference: https://docs.astro.build/en/guides/typescript/#component-props
// WRONG
---
const { title, items } = Astro.props;
// What if title is undefined?
---
// RIGHT
---
interface Props {
title: string;
items: string[];
}
const { title, items } = Astro.props as Props;
---
Detection: async function without try/catch or .catch()
Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
// WRONG
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Unhandled rejection on error!
}
// RIGHT
async function fetchUser(id: string) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error('User not found');
return response.json();
} catch (error) {
console.error('Failed to fetch user', error);
throw error;
}
}
any Instead of Proper Types (P2)Detection: Type annotation with any
Reference: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#any
// WRONG
function processData(data: any) {
return data.name; // No type safety!
}
// RIGHT
interface User {
name: string;
age: number;
}
function processData(data: User) {
return data.name;
}
Detection: Assignment to function parameters Reference: https://www.typescriptlang.org/docs/handbook/2/functions.html#parameter-destructuring
// WRONG
function processUser(user: User) {
user.name = 'Modified'; // Mutates original!
}
// RIGHT
function processUser(user: User): User {
return { ...user, name: 'Modified' };
}
Detection: Object/array creation inside render/return Reference: https://react.dev/learn/keeping-components-pure
// WRONG — new object created on every render
const Child = ({ name, id }) => (
<UserCard user={{ name, id }} />
);
// RIGHT — move outside the component (module constant)
const defaultUser = { name: 'John', id: 1 };
const Child = () => <UserCard user={defaultUser} />;
// Or RIGHT — memoize when values change per render
const Child = ({ name, id }) => (
<UserCard user={useMemo(() => ({ name, id }), [name, id])} />
);
Detection: Manual null check before accessing nested property Reference: https://www.typescriptlang.org/docs/handbook/2/functions.html#optional-parameters
// WRONG — verbose null checks
const name = user && user.profile && user.profile.name;
// RIGHT — optional chaining
const name = user?.profile?.name;
Detection: Inconsistent use of null and undefined Reference: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#null-and-undefined
// WRONG — mixing null and undefined
function createUser(name: string, age?: number | null) {
// Confusing when to use which
}
// RIGHT — consistent approach
function createUser(name: string, age?: number) {
// Use undefined for optional, value for required
}
Detection: Magic numbers inside styled-components / Emotion / CSS modules Reference: https://styled-components.com/docs/basics
// WRONG — magic number inside a styled-component
const Card = styled.div`padding: 8px;`;
// RIGHT — use design token
const Card = styled.div`padding: var(--ds-space-2);`;
Detection: Raw px breakpoints in media queries instead of design tokens
Reference: https://developer.mozilla.org/docs/Web/CSS/@media
/* WRONG — duplicated magic breakpoint */
@media (min-width: 768px) { }
/* RIGHT — breakpoint token */
@media (min-width: var(--ds-breakpoint-md)) { }
Detection: Interactive element smaller than the 44px mobile touch target Reference: https://developer.apple.com/design/human-interface-guidelines/touch-targets
/* WRONG — too small to tap reliably */
.close-btn { width: 32px; height: 32px; }
/* RIGHT — minimum mobile touch target */
.close-btn { width: 44px; height: 44px; }
| Severity | Meaning | Examples |
|---|---|---|
| P0 | Blocking — CI will fail | Bare oklch in component rule, broken dark mode override, critical API misuse |
| P1 | High — must fix | Missing aria-label, invalid BEM, hardcoded spacing, hook deps missing |
| P2 | Medium — should fix | Duplicate rules, empty catch blocks, semantic violations |
| P3 | Low — polish | Code style, cosmetic issues |
## Argus Design Review Summary
- Total Issues: N (P0: X | P1: X | P2: X | P3: X)
- Files Reviewed: N
- Technology Stack: {detected stack}
- Documentation: {official docs URL}
Issues are grouped under headers in order: P0 → P1 → P2 → P3.
## P0 — Blocking Issues (must fix, CI will fail)
## P1 — High Priority (must fix before merge)
## P2 — Medium Priority (should fix)
## P3 — Low Priority (optional polish)
[P{severity}] {file}:{line} — {short description}
Found: {current code snippet}
Expected: {correct code snippet}
Fix:
```{extension}
{copy-ready fix code}
Token: {design token to use, if applicable} Reference: {official docs URL for this API} Note: {optional context or explanation}
### Format Rules
- Each issue block starts with a `─────────────────────────────────────────────────` separator line
- Code snippets are shown inline, truncated to relevant portion (max 80 chars per line)
- **Fix code block is mandatory** — always provide the exact fix to copy
- Empty `Note:` line is omitted if not needed
- No issue = output `✓ No issues found` under each severity group
- Always include `Reference:` link when flagging framework API issues
## Review Workflow
1. **Detect stack** — scan file extensions and package.json
2. Read the codebase — understand the design token system in use
3. Scan for bare color values (oklch/hex/rgb outside :root declarations)
4. Scan for magic numbers in spacing, radii, font sizes
5. Verify dark mode coverage for every color token
6. Check accessibility — buttons, images, semantic HTML
7. Check CSS quality — duplicates, BEM, empty catch blocks
8. **Stack-specific API checks** — verify hooks, directives, lifecycle usage against Framework Anti-Patterns Library
9. **Generate fixes** — provide copy-ready code for every issue found
10. Report findings grouped by severity
**In automated PR review mode:** The composite action at `.github/actions/argus-review/action.yml` reads `AGENTS.md` + `SKILL.md` from the argus repo at runtime and injects their contents into the LLM prompt. The review is performed by the `argus-flash` GitHub App, which comments findings directly on the PR.
## Non-Blocking Context
Do NOT flag issues in:
- Third-party resets or normalize.css
- Generated boilerplate that will be replaced
- Test fixtures and mock data files
- `node_modules/` (ignore entirely)
- Workflow YAML files (`.github/workflows/`, `.github/actions/`)