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
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ Phase 6: `GET /api/agent/tree` 감독용 조회 API + SSE 재연결/오류복구
이 세션(PM)의 역할은 분석·계획·검토다. 코드 편집은 Dev 세션에 위임한다.

- **분석**: 관련 파일을 Read로 읽어 현재 구조와 맥락을 파악한다
- **스펙 작성**: 대상 파일·변경 내용·이유·완료 기준을 포함한 구현 스펙을 작성한다
- **Dev 세션 호출**: `cd {project_dir} && claude -p "{스펙}" --model sonnet --output-format json --dangerously-skip-permissions`
- **결과 검토**: 변경된 파일을 Read로 확인하여 스펙 준수 여부와 코드 품질을 검토한다
- **PRD 수신**: 오케스트레이터로부터 PRD(`~/workspace/statpan_docs/projects/_ORCHESTRATION/templates/PRD.md` 양식)를 받는다. PRD에는 목표 상태·Acceptance Criteria·Scope 경계가 포함된다.
- **Tech Spec 작성**: PRD를 기반으로 `~/workspace/statpan_docs/projects/_ORCHESTRATION/templates/TECH_SPEC.md` 양식에 맞춰 Tech Spec을 작성한다. 저장 위치: `~/workspace/statpan_docs/projects/agentree/TechSpecs/`
- **Dev 세션 호출**: Tech Spec 전문을 전달한다. `cd {project_dir} && claude -p "$(cat {tech_spec_path})" --model sonnet --output-format json --dangerously-skip-permissions`
- **결과 검토**: Tech Spec의 Acceptance Criteria 항목별 evidence를 확인한다. evidence 없는 완료 보고는 불인정한다.

코드를 직접 Edit/Write/Bash로 수정하지 않는다.

Expand Down
2 changes: 1 addition & 1 deletion src/client/canvas/AgentCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export function AgentCanvas() {
{sseStatus !== 'connected' && (
<div style={{
position: 'absolute', top: treeError ? 60 : 16, left: '50%', transform: 'translateX(-50%)', zIndex: 20,
background: sseStatus === 'disconnected' ? '#1c1917' : '#1c1917',
background: '#1c1917',
border: `1px solid ${sseStatus === 'disconnected' ? '#ef4444' : '#f59e0b'}`,
color: sseStatus === 'disconnected' ? '#fca5a5' : '#fcd34d',
padding: '6px 14px', borderRadius: 8,
Expand Down
11 changes: 11 additions & 0 deletions src/server/routes/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,17 @@ describe('GET /api/agent/tree', () => {
expect(body.pendingQuestions[0].requestId).toBe('q-1')
})

it('overrides session status when a pending question exists', async () => {
vi.mocked(getPendingQuestions).mockReturnValue([
{ requestId: 'q-1', sessionId: 'sess-1', message: 'Which branch?', metadata: {} },
])
const res = await app.request('/api/agent/tree')
expect(res.status).toBe(200)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = await res.json() as any
expect(body.sessions.find((s: { id: string }) => s.id === 'sess-1')?.status).toBe('needs-answer')
})

it('overrides session status when a pending permission exists', async () => {
vi.mocked(getPendingPermissions).mockReturnValue([
{ requestId: 'req-1', sessionId: 'sess-1', message: 'Allow file write?', metadata: {} },
Expand Down
41 changes: 2 additions & 39 deletions src/server/routes/agent.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import { Hono } from 'hono'
import { getAllCanvasNodes, getAllProjects, getAllSessionRelations, getAllTaskInvocations, getForkRelationMap, findOrCreateProject, setCanvasNodeProject } from '../db/index.js'
import { getAllCanvasNodes, getAllProjects, getAllSessionRelations, getAllTaskInvocations, getForkRelationMap } from '../db/index.js'
import { opencodeAdapter } from '../opencode/index.js'
import { getPendingPermissions, getPendingQuestions } from '../sse/broadcaster.js'
import { projectGroupFromDirectory } from '../utils/projectGroup.js'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The import projectGroupFromDirectory is unused in this file. The logic that previously required it (project auto-creation) was removed in this PR to eliminate side effects from the GET request. This import should be removed to keep the code clean.


export const agentRouter = new Hono()

function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
const bucketPrefixes = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
if (parts.length >= 2 && bucketPrefixes.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}

/**
* GET /api/agent/tree
* Compact supervisor-agent view of the session tree.
Expand All @@ -41,16 +29,6 @@ agentRouter.get('/api/agent/tree', async (c) => {
const sessions = sessionsResult.value
const statusBySession = statusResult.status === 'fulfilled' ? statusResult.value : {}

// Auto-create/resolve projects (same logic as tree.ts)
const projectByDirectoryKey = new Map<string, { id: string; name: string }>()
for (const session of sessions) {
const dirKey = projectGroupFromDirectory(session.directory)
if (!projectByDirectoryKey.has(dirKey)) {
const proj = findOrCreateProject(dirKey)
projectByDirectoryKey.set(dirKey, proj)
}
}

const canvasBySession = new Map(
getAllCanvasNodes().map((node) => [
node.session_id,
Expand All @@ -63,21 +41,6 @@ agentRouter.get('/api/agent/tree', async (c) => {
]),
)

for (const session of sessions) {
const dirKey = projectGroupFromDirectory(session.directory)
const proj = projectByDirectoryKey.get(dirKey)
if (!proj) continue
const canvas = canvasBySession.get(session.id)
if (!canvas?.projectId) {
setCanvasNodeProject(session.id, proj.id)
if (canvas) {
canvas.projectId = proj.id
} else {
canvasBySession.set(session.id, { label: null, pinned: false, detached: false, projectId: proj.id })
}
}
}

const relations = getAllSessionRelations()
const taskInvocations = getAllTaskInvocations()
const projects = getAllProjects()
Expand Down
14 changes: 1 addition & 13 deletions src/server/routes/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,10 @@ import { Hono } from 'hono'
import { getAllCanvasNodes, getAllProjects, getForkRelationMap, getAllSessionRelations, getAllTaskInvocations, findOrCreateProject, setCanvasNodeProject } from '../db/index.js'
import type { ProjectRow } from '../db/schema.js'
import { opencodeAdapter } from '../opencode/index.js'
import { projectGroupFromDirectory } from '../utils/projectGroup.js'

export const treeRouter = new Hono()

function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
const bucketPrefixes = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
if (parts.length >= 2 && bucketPrefixes.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}

treeRouter.get('/api/tree', async (c) => {
const [sessionsResult, statusResult, compatResult] = await Promise.allSettled([
opencodeAdapter.listSessions(),
Expand Down
12 changes: 12 additions & 0 deletions src/server/utils/projectGroup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
const bucketPrefixes = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
if (parts.length >= 2 && bucketPrefixes.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}
Comment on lines +1 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The bucketPrefixes Set is currently allocated on every call to projectGroupFromDirectory. Since this function is called within loops (e.g., in tree.ts), moving this set to a constant outside the function scope will avoid unnecessary allocations and improve performance.

Suggested change
export function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
const bucketPrefixes = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
if (parts.length >= 2 && bucketPrefixes.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}
const BUCKET_PREFIXES = new Set(['apps', 'research', 'pypi_lib', 'libs', 'infra', 'skills', 'mcps', 'anal-repo'])
export function projectGroupFromDirectory(directory: string): string {
const marker = '/workspace/'
const index = directory.indexOf(marker)
const normalized = index >= 0 ? directory.slice(index + marker.length) : directory.replace(/^\/+/, '')
const parts = normalized.split('/').filter(Boolean)
if (parts.length === 0) return 'workspace'
if (parts.length >= 2 && BUCKET_PREFIXES.has(parts[0])) {
return `${parts[0]}/${parts[1]}`
}
return parts[0]
}

Loading