Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
adc9b08
⚡ Bolt: Optimize react-window Row rendering in EventList
seonghobae May 28, 2026
39b471b
⚡ Bolt: Optimize event list rendering performance
seonghobae May 28, 2026
1d3cc4e
⚡ Bolt: Optimize event list rendering performance
seonghobae May 30, 2026
677977b
Fix EventList Row memoization typing/comparator
Codex May 30, 2026
97eaed3
⚡ Bolt: Optimize event list rendering performance
seonghobae May 30, 2026
f79edf3
⚡ Bolt: Optimize event list rendering performance
seonghobae May 30, 2026
cdf4ce8
chore(deps): next 15.5.15 → 15.5.18 보안 업데이트 (High 7건)
seonghobae May 31, 2026
38eea28
fix: 리뷰 반영 - selectedIdx useMemo 의존성 제거, comparator 수정, .jules/bolt.m…
seonghobae Jun 1, 2026
d438567
⚡ Bolt: Optimize event list rendering performance
seonghobae Jun 1, 2026
ba44a74
fix: replace 'as any' cast with 'as typeof Row' for MemoizedRow type …
Copilot Jun 1, 2026
72f10bc
fix: MemoizedRow 타입 오류 수정 - react-window v2 rowComponent 호환
seonghobae Jun 1, 2026
4711813
🎨 접근성 개선: OverviewStats 펼치기 버튼에 ARIA 속성 추가 (#20)
seonghobae Jun 1, 2026
faa8108
⚡ 성능 최적화: usageRecords 다중 배열 순회를 단일 루프로 통합 (#25)
seonghobae Jun 1, 2026
146a5fb
ci: 의존성 보안 스캔 워크플로우 추가 (#26)
seonghobae Jun 1, 2026
ea2dd74
fix: OSV 취약 의존성 업데이트 (#27)
seonghobae Jun 1, 2026
e7618dd
fix: EventList 리뷰 반영 - .jules 무시 및 MemoizedRow 단순화
seonghobae Jun 1, 2026
60dd3e1
fix: Strix 리뷰에서 분리한 앱 버그 수정 (#28)
seonghobae Jun 1, 2026
6e3ea23
ci: dependency-review 미지원 저장소에서 비차단 처리 (#29)
seonghobae Jun 1, 2026
f70a825
🛡️ [CRITICAL] 관리자 아이디·비밀번호 하드코딩 자격증명 완전 제거 (#19)
seonghobae Jun 1, 2026
58b3658
Merge remote-tracking branch 'upstream/main' into update-pr21-origin
seonghobae Jun 1, 2026
281093e
fix: EventList 메모이제이션 리뷰 수정 재적용
seonghobae Jun 1, 2026
11c2830
Merge remote-tracking branch 'upstream/main' into update-pr21-origin
seonghobae Jun 1, 2026
79d0bd4
⚡ Bolt: Optimize event list rendering performance
seonghobae Jun 1, 2026
5a04382
fix(web): turbo.json에 ADMIN_USERNAME/ADMIN_PASSWORD env 변수 추가
greatSumini Jun 1, 2026
138f013
Revert "⚡ Bolt: Optimize event list rendering performance"
seonghobae Jun 1, 2026
3ca3dec
Merge remote-tracking branch 'upstream/main' into update-pr21-origin
seonghobae Jun 1, 2026
f812856
fix: EventList 선택 그룹 자동 펼침 유지
seonghobae Jun 1, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,5 @@ jobs:
JWT_SECRET: "ci-placeholder-jwt-secret-min-32-chars"
DATABASE_URL: "postgresql://placeholder:placeholder@localhost:5432/placeholder"
DIRECT_URL: "postgresql://placeholder:placeholder@localhost:5432/placeholder"
ADMIN_USERNAME: "ci-admin"
ADMIN_PASSWORD: "ci-admin-password"
27 changes: 27 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Dependency Review

on:
pull_request:
branches: [main]

permissions:
contents: read
pull-requests: read

jobs:
dependency-review:
name: dependency-review
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Dependency review
continue-on-error: true
uses: actions/dependency-review-action@v4
with:
fail-on-severity: moderate
- name: Dependency review availability note
if: always()
run: |
echo "Dependency Review requires GitHub Dependency Graph to be enabled for this repository."
echo "OSV-Scanner remains the blocking dependency vulnerability gate."
24 changes: 24 additions & 0 deletions .github/workflows/osvscanner.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: OSV-Scanner

on:
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read
security-events: write

jobs:
scan:
name: scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
- name: Run OSV-Scanner
uses: google/osv-scanner-action/osv-scanner-action@9a498708959aeaef5ef730655706c5a1df1edbc2
with:
scan-args: |-
--recursive
.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ backups/
persuasion-data/runs/
__pycache__/
*.pyc
.jules/
11 changes: 11 additions & 0 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module.exports = {
hooks: {
readPackage(pkg) {
if (pkg.name === 'next' && pkg.version === '15.5.18') {
pkg.dependencies = pkg.dependencies || {}
pkg.dependencies.postcss = '8.5.15'
}
return pkg
},
},
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"eslint": "^9",
"turbo": "^2",
"turbo": "^2.9.16",
"typescript-eslint": "^8"
},
"packageManager": "pnpm@9.15.4+sha512.b2dc20e2fc72b3e18848459b37359a32064663e5627a51e4c74b2c29dd8e8e0491483c3abb40789cfd578bf362fb6ba8261b05f0387d76792ed6e23ea3b1b6a0"
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@argos/shared": "workspace:*",
"@types/node": "^20",
"typescript": "^5",
"vitest": "^2.1.9"
"vitest": "^3.2.6"
},
"engines": {
"node": ">=18"
Expand Down
29 changes: 28 additions & 1 deletion packages/cli/src/lib/event-sender.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@ import { describe, it, expect, beforeAll } from 'vitest'
import { buildSelfHealScript } from './event-sender.js'

const TMP_FILE = '/tmp/argos-test-payload.json'
const TMP_DIR = '/tmp/argos-test-dir'
const PROJECT_JSON_PATH = '/repo/.argos/project.json'

describe('buildSelfHealScript', () => {
let script: string

beforeAll(() => {
script = buildSelfHealScript({ tmpFile: TMP_FILE, projectJsonPath: PROJECT_JSON_PATH })
script = buildSelfHealScript({
tmpFile: TMP_FILE,
tmpDir: TMP_DIR,
projectJsonPath: PROJECT_JSON_PATH,
})
})

it('returns a non-empty string', () => {
Expand All @@ -34,6 +39,19 @@ describe('buildSelfHealScript', () => {
expect(script).toContain('renameSync')
})

it('holds an inter-process lock while rewriting project.json', () => {
expect(script).toContain(`const lockDir=${JSON.stringify(PROJECT_JSON_PATH)}+'.lock'`)
const mkdirIdx = script.indexOf('fs.mkdirSync(lockDir)')
const readIdx = script.indexOf(`JSON.parse(fs.readFileSync(${JSON.stringify(PROJECT_JSON_PATH)},'utf8'))`)
const renameIdx = script.indexOf(`fs.renameSync(atomicTmp,${JSON.stringify(PROJECT_JSON_PATH)})`)
const releaseIdx = script.indexOf('fs.rmdirSync(lockDir)')

expect(mkdirIdx).toBeGreaterThanOrEqual(0)
expect(readIdx).toBeGreaterThan(mkdirIdx)
expect(renameIdx).toBeGreaterThan(readIdx)
expect(releaseIdx).toBeGreaterThan(renameIdx)
})

it('(d) contains res.status !== 202 guard', () => {
expect(script).toContain('res.status!==202')
})
Expand Down Expand Up @@ -76,6 +94,15 @@ describe('buildSelfHealScript', () => {
expect(afterFinally).toContain('unlinkSync')
})

it('cleans up the private tmp directory in finally block', () => {
const finallyIdx = script.indexOf('finally')
const afterFinally = script.slice(finallyIdx)
expect(afterFinally).toContain(TMP_DIR)
expect(afterFinally).toContain('rmSync')
expect(afterFinally).toContain('recursive:true')
expect(afterFinally).toContain('force:true')
})

it('is wrapped in an async IIFE', () => {
expect(script).toContain('async()')
})
Expand Down
43 changes: 29 additions & 14 deletions packages/cli/src/lib/event-sender.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync, unlinkSync, writeFileSync } from 'fs'
import { mkdtempSync, rmSync, writeFileSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
import { spawn } from 'child_process'
Expand Down Expand Up @@ -48,13 +48,16 @@ export interface SendEventBackgroundOpts {
*/
export function buildSelfHealScript({
tmpFile,
tmpDir,
projectJsonPath,
}: {
tmpFile: string
tmpDir?: string
projectJsonPath: string
}): string {
// Serialize paths as JSON so they are safely embedded in the script string.
const tmpFileJson = JSON.stringify(tmpFile)
const tmpDirJson = tmpDir ? JSON.stringify(tmpDir) : 'null'
const projectJsonPathJson = JSON.stringify(projectJsonPath)

return [
Expand All @@ -75,24 +78,32 @@ export function buildSelfHealScript({
`if(!body||!body.project||typeof body.project.id!=='string'||typeof body.project.orgId!=='string'||typeof body.project.orgSlug!=='string')return;`,
// Step 5: Guard — must be for the same project (cross-project contamination check)
`if(body.project.id!==currentConfig.projectId)return;`,
// Step 6: Re-read projectJsonPath (race protection for concurrent hooks)
// Step 6: Acquire a project-local lock before re-reading and rewriting.
// mkdirSync is atomic across processes and prevents concurrent self-heal
// writers from overwriting each other's updates between read and rename.
`const lockDir=${projectJsonPathJson}+'.lock';`,
`let locked=false;`,
`for(let i=0;i<20;i++){try{fs.mkdirSync(lockDir);locked=true;break;}catch(e){if(!e||e.code!=='EEXIST')return;await new Promise(r=>setTimeout(r,25));}}`,
`if(!locked)return;`,
`let atomicTmp;`,
`try{`,
// Step 7: Re-read projectJsonPath (race protection for concurrent hooks)
`let latest;`,
`try{latest=JSON.parse(fs.readFileSync(${projectJsonPathJson},'utf8'));}catch{return;}`,
// Step 7: Guard — projectId must still match after re-read
// Step 8: Guard — projectId must still match after re-read
`if(latest.projectId!==body.project.id)return;`,
// Step 8: No-op if already up to date (idempotent)
// Step 9: No-op if already up to date (idempotent)
`if(latest.orgId===body.project.orgId&&latest.orgSlug===body.project.orgSlug)return;`,
// Step 9: Merge new orgId/orgSlug, preserving all other fields and key order
// Step 10: Merge new orgId/orgSlug, preserving all other fields and key order
`const updated={...latest,orgId:body.project.orgId,orgSlug:body.project.orgSlug};`,
// Step 10: Atomic write via tmp + renameSync
`const atomicTmp=${projectJsonPathJson}+'.tmp.'+process.pid+'.'+Math.random().toString(36).slice(2);`,
`try{`,
// Step 11: Atomic write via tmp + renameSync
`atomicTmp=${projectJsonPathJson}+'.tmp.'+process.pid+'.'+Math.random().toString(36).slice(2);`,
`fs.writeFileSync(atomicTmp,JSON.stringify(updated,null,2),'utf8');`,
`fs.renameSync(atomicTmp,${projectJsonPathJson});`,
`}catch{try{fs.unlinkSync(atomicTmp);}catch{}}`,
`}catch{try{if(atomicTmp)fs.unlinkSync(atomicTmp);}catch{}}finally{try{fs.rmdirSync(lockDir);}catch{}}`,
`}catch{}`,
// Cleanup tmp file in finally (runs whether self-heal succeeded or any early return)
`finally{try{fs.unlinkSync(${tmpFileJson});}catch{}}`,
// Cleanup tmp file/dir in finally (runs whether self-heal succeeded or any early return)
`finally{try{fs.unlinkSync(${tmpFileJson});}catch{};if(${tmpDirJson})try{fs.rmSync(${tmpDirJson},{recursive:true,force:true});}catch{}}`,
`})()`,
].join('')
}
Expand All @@ -109,22 +120,26 @@ export function buildSelfHealScript({
export function sendEventBackground(opts: SendEventBackgroundOpts): void {
const { url, token, payload, projectJsonPath, currentConfig } = opts

const tmpFile = join(tmpdir(), `argos-${Date.now()}-${Math.random().toString(36).slice(2)}.json`)
let tmpDir: string | undefined
try {
tmpDir = mkdtempSync(join(tmpdir(), 'argos-'))
const tmpFile = join(tmpDir, 'payload.json')
writeFileSync(
tmpFile,
JSON.stringify({ url, token, payload, projectJsonPath, currentConfig }),
'utf8',
)

const script = buildSelfHealScript({ tmpFile, projectJsonPath })
const script = buildSelfHealScript({ tmpFile, tmpDir, projectJsonPath })

const child = spawn(process.execPath, ['-e', script], {
detached: true,
stdio: 'ignore',
})
child.unref()
} catch {
try { if (existsSync(tmpFile)) unlinkSync(tmpFile) } catch {}
try {
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
} catch {}
}
}
39 changes: 39 additions & 0 deletions packages/cli/src/lib/project.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { afterEach, describe, expect, it } from 'vitest'
import { mkdtempSync, mkdirSync, realpathSync, rmSync } from 'fs'
import { join, resolve } from 'path'
import { tmpdir } from 'os'
import { findProjectConfigWithPath, writeProjectConfig } from './project.js'

const originalCwd = process.cwd()

afterEach(() => {
process.chdir(originalCwd)
})

describe('findProjectConfigWithPath', () => {
it('resolves relative startDir segments before walking parent directories', () => {
const tmpRoot = mkdtempSync(join(tmpdir(), 'argos-project-test-'))

try {
const repoRoot = join(tmpRoot, 'repo')
const nestedDir = join(repoRoot, 'packages', 'cli')
mkdirSync(nestedDir, { recursive: true })
writeProjectConfig({
projectId: 'project-1',
orgId: 'org-1',
orgName: 'Org',
projectName: 'Project',
}, repoRoot)

process.chdir(repoRoot)

const result = findProjectConfigWithPath('packages/../packages/cli')

expect(result?.config.projectId).toBe('project-1')
expect(result?.configPath).toBe(realpathSync(resolve(repoRoot, '.argos', 'project.json')))
} finally {
process.chdir(originalCwd)
rmSync(tmpRoot, { recursive: true, force: true })
}
})
})
4 changes: 2 additions & 2 deletions packages/cli/src/lib/project.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dirname, join } from 'path'
import { dirname, join, resolve } from 'path'
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
import { normalizeApiUrl } from './config.js'

Expand All @@ -22,7 +22,7 @@ export interface ProjectConfig {
export function findProjectConfigWithPath(
startDir?: string,
): { config: ProjectConfig; configPath: string } | null {
let currentDir = startDir || process.cwd()
let currentDir = resolve(startDir || process.cwd())
let depth = 0
const maxDepth = 10

Expand Down
2 changes: 2 additions & 0 deletions packages/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ AUTH_SECRET=replace-with-min-32-char-random-string
DATABASE_URL="postgresql://postgres.[project]:[password]@aws-0-ap-northeast-1.pooler.supabase.com:6543/postgres?pgbouncer=true"
DIRECT_URL="postgresql://postgres.[project]:[password]@db.uwxfseowdzuuepeeudrx.supabase.co:5432/postgres"
JWT_SECRET="replace-with-32-char-minimum-random-string"
ADMIN_USERNAME="admin"
ADMIN_PASSWORD="replace-with-secure-admin-password"
4 changes: 2 additions & 2 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"recharts": "^2",
"remark-gfm": "^4.0.1",
"server-only": "^0.0.1",
"shadcn": "^4.2.0",
"shadcn": "^4.10.0",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"zod": "^3"
Expand All @@ -51,6 +51,6 @@
"prisma": "^6",
"tailwindcss": "^4",
"typescript": "^5",
"vitest": "^2.1.9"
"vitest": "^3.2.6"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,25 @@ export async function GET(
return forbiddenByRole(access.role, '본인 세션만 열람 가능')
}

const totalInput = session.usageRecords.reduce((sum, r) => sum + r.inputTokens, 0)
const totalOutput = session.usageRecords.reduce((sum, r) => sum + r.outputTokens, 0)
const totalCost = session.usageRecords.reduce((sum, r) => sum + (r.estimatedCostUsd ?? 0), 0)

const usageTimeline: SessionTimelineUsage[] = session.usageRecords.map((r) => ({
timestamp: r.timestamp.toISOString(),
inputTokens: r.inputTokens,
outputTokens: r.outputTokens,
estimatedCostUsd: r.estimatedCostUsd ?? 0,
model: r.model,
isSubagent: r.isSubagent,
}))
let totalInput = 0
let totalOutput = 0
let totalCost = 0
const usageTimeline: SessionTimelineUsage[] = new Array(session.usageRecords.length)

for (let i = 0; i < session.usageRecords.length; i++) {
const r = session.usageRecords[i]
totalInput += r.inputTokens
totalOutput += r.outputTokens
totalCost += r.estimatedCostUsd ?? 0
usageTimeline[i] = {
timestamp: r.timestamp.toISOString(),
inputTokens: r.inputTokens,
outputTokens: r.outputTokens,
estimatedCostUsd: r.estimatedCostUsd ?? 0,
model: r.model,
isSubagent: r.isSubagent,
}
}

// 각 UsageRecord를 "직전 ASSISTANT 턴"에 귀속시켜 메시지별 토큰/비용/모델 집계.
// TOOL 메시지는 건너뛰고 가장 가까운 선행 ASSISTANT로 타고 올라감.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,17 @@ const sessionInclude = {
type SessionWithInclude = Prisma.ClaudeSessionGetPayload<{ include: typeof sessionInclude }>

function getSessionTotals(session: SessionWithInclude) {
return {
inputTokens: session.usageRecords.reduce((sum, r) => sum + r.inputTokens, 0),
outputTokens: session.usageRecords.reduce((sum, r) => sum + r.outputTokens, 0),
estimatedCostUsd: session.usageRecords.reduce(
(sum, r) => sum + (r.estimatedCostUsd ?? 0),
0,
),
let inputTokens = 0
let outputTokens = 0
let estimatedCostUsd = 0

for (const r of session.usageRecords) {
inputTokens += r.inputTokens
outputTokens += r.outputTokens
estimatedCostUsd += r.estimatedCostUsd ?? 0
}

return { inputTokens, outputTokens, estimatedCostUsd }
}

function mapSessionItem(session: SessionWithInclude): SessionItem {
Expand Down
Loading