diff --git a/CLAUDE.md b/CLAUDE.md
index 2616598..0f1e7c8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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로 수정하지 않는다.
diff --git a/src/client/canvas/AgentCanvas.tsx b/src/client/canvas/AgentCanvas.tsx
index bf81f06..8ef6d06 100644
--- a/src/client/canvas/AgentCanvas.tsx
+++ b/src/client/canvas/AgentCanvas.tsx
@@ -322,7 +322,7 @@ export function AgentCanvas() {
{sseStatus !== 'connected' && (
{
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: {} },
diff --git a/src/server/routes/agent.ts b/src/server/routes/agent.ts
index aa5e336..a6ad24d 100644
--- a/src/server/routes/agent.ts
+++ b/src/server/routes/agent.ts
@@ -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'
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.
@@ -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()
- 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,
@@ -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()
diff --git a/src/server/routes/tree.ts b/src/server/routes/tree.ts
index 63836ca..fc1b12e 100644
--- a/src/server/routes/tree.ts
+++ b/src/server/routes/tree.ts
@@ -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(),
diff --git a/src/server/utils/projectGroup.ts b/src/server/utils/projectGroup.ts
new file mode 100644
index 0000000..24e5cdf
--- /dev/null
+++ b/src/server/utils/projectGroup.ts
@@ -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]
+}