tycono 0.1.93-beta.0 → 0.1.93-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.93-beta.0",
3
+ "version": "0.1.93-beta.1",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -595,6 +595,84 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
595
595
 
596
596
  onTurnComplete?.(turns);
597
597
  }
598
+
599
+ // ── Post-Knowledging: ④⑤ 수행 프롬프트 (KP-008) ──
600
+ // Engineer/CTO가 구현 완료 후 The Loop 마무리 (Knowledge 업데이트 + Task 상태 갱신)를 수행하도록 유도
601
+ const postKPrompt = [
602
+ '[POST-KNOWLEDGING] 구현이 완료되었습니다. The Loop 마무리를 수행하세요:',
603
+ '',
604
+ '## ④ Knowledge 업데이트 (The Loop Step 4)',
605
+ '다음 중 해당하는 항목을 수행하세요:',
606
+ '- 본인 journal 업데이트 (`roles/' + roleId + '/journal/YYYY-MM-DD.md` — 오늘 날짜 파일)',
607
+ '- 구현 중 새로 발견한 패턴/아키텍처 결정이 있다면 관련 문서 업데이트',
608
+ ' (예: architecture/web-app-ia.md, architecture/session-worktree-isolation.md 등)',
609
+ '- 중요한 기술 결정은 operations/decisions/ 또는 architecture/ 반영',
610
+ '',
611
+ '## ⑤ Task 상태 갱신 (The Loop Step 5)',
612
+ '- `projects/tycono-platform/tasks.md` (또는 관련 tasks 파일)에서 완료한 태스크 상태를 DONE으로 변경',
613
+ '- 다음 작업이 있다면 식별하여 메모',
614
+ '',
615
+ '⛔ **필수**: ④와 ⑤를 모두 수행해야 The Loop이 완료됩니다.',
616
+ '이제 ④⑤를 수행하세요 (Read, Edit 도구 사용).',
617
+ ].join('\n');
618
+
619
+ messages.push({ role: 'user', content: postKPrompt });
620
+
621
+ // Run Post-K loop (최대 2턴 — C-Level의 3턴보다 짧게)
622
+ const maxPostKRounds = 2;
623
+ for (let round = 0; round < maxPostKRounds && turns < maxTurns; round++) {
624
+ if (abortSignal?.aborted) break;
625
+ turns++;
626
+
627
+ const postKResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
628
+ totalInput += postKResponse.usage.inputTokens;
629
+ totalOutput += postKResponse.usage.outputTokens;
630
+ config.tokenLedger?.record({
631
+ ts: new Date().toISOString(),
632
+ sessionId: config.sessionId,
633
+ roleId,
634
+ model: config.model ?? 'unknown',
635
+ inputTokens: postKResponse.usage.inputTokens,
636
+ outputTokens: postKResponse.usage.outputTokens,
637
+ });
638
+
639
+ messages.push({ role: 'assistant', content: postKResponse.content });
640
+ for (const block of postKResponse.content) {
641
+ if (block.type === 'text' && block.text) {
642
+ outputParts.push(block.text);
643
+ onText?.(block.text);
644
+ }
645
+ }
646
+
647
+ // If no tool calls, Post-K is done
648
+ if (postKResponse.stopReason !== 'tool_use') break;
649
+
650
+ // Execute Post-K tool calls
651
+ const postKToolCalls = postKResponse.content.filter(
652
+ (b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
653
+ );
654
+ const postKResults: ToolResult[] = [];
655
+ for (const tc of postKToolCalls) {
656
+ allToolCalls.push({ name: tc.name, input: tc.input });
657
+ const result = await executeTool(
658
+ { id: tc.id, name: tc.name, input: tc.input },
659
+ toolExecOptions,
660
+ );
661
+ postKResults.push(result);
662
+ }
663
+
664
+ messages.push({
665
+ role: 'user',
666
+ content: postKResults.map((r) => ({
667
+ type: 'tool_result' as const,
668
+ tool_use_id: r.tool_use_id,
669
+ content: r.content,
670
+ is_error: r.is_error,
671
+ })) as unknown as MessageContent[],
672
+ });
673
+
674
+ onTurnComplete?.(turns);
675
+ }
598
676
  }
599
677
  }
600
678
  }
@@ -10,6 +10,7 @@ import {
10
10
  createSession,
11
11
  addMessage,
12
12
  updateMessage,
13
+ listSessions,
13
14
  type Message,
14
15
  type ImageAttachment,
15
16
  } from '../services/session-store.js';
@@ -48,6 +49,24 @@ export function handleExecRequest(req: IncomingMessage, res: ServerResponse): vo
48
49
 
49
50
  // ── /api/waves/active — restore active waves after refresh ──
50
51
  if (method === 'GET' && url === '/api/waves/active') {
52
+ // Recovery: rebuild wave→session mapping from session-store if lost
53
+ const waves = waveMultiplexer.getActiveWaves();
54
+ if (waves.length === 0) {
55
+ const allSessions = listSessions();
56
+ const waveGroups = new Map<string, string[]>();
57
+ for (const ses of allSessions) {
58
+ if (ses.waveId) {
59
+ if (!waveGroups.has(ses.waveId)) waveGroups.set(ses.waveId, []);
60
+ waveGroups.get(ses.waveId)!.push(ses.id);
61
+ }
62
+ }
63
+ for (const [wid, sids] of waveGroups) {
64
+ for (const sid of sids) {
65
+ const exec = executionManager.getActiveExecution(sid);
66
+ if (exec) waveMultiplexer.registerSession(wid, exec);
67
+ }
68
+ }
69
+ }
51
70
  jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
52
71
  return;
53
72
  }
@@ -399,7 +418,22 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
399
418
  const fromMatch = url.match(/[?&]from=(\d+)/);
400
419
  const fromWaveSeq = fromMatch ? parseInt(fromMatch[1], 10) : 0;
401
420
 
402
- const sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
421
+ let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
422
+
423
+ // Recovery: if wave→session mapping was lost (e.g. server restart),
424
+ // rebuild from session-store (sessions have waveId) + executionManager
425
+ if (sessionIds.length === 0) {
426
+ const allSessions = listSessions();
427
+ const waveSessions = allSessions.filter(s => s.waveId === waveId);
428
+ for (const ses of waveSessions) {
429
+ const exec = executionManager.getActiveExecution(ses.id);
430
+ if (exec) {
431
+ waveMultiplexer.registerSession(waveId, exec);
432
+ }
433
+ }
434
+ sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
435
+ }
436
+
403
437
  if (sessionIds.length === 0) {
404
438
  res.writeHead(404, { 'Content-Type': 'application/json' });
405
439
  res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
@@ -683,7 +717,10 @@ function handleStatus(res: ServerResponse): void {
683
717
 
684
718
  const activeExecs = executionManager.listExecutions({ active: true });
685
719
  for (const exec of activeExecs) {
686
- statuses[exec.roleId] = messageStatusToRoleStatus(exec.status as MessageStatus);
720
+ // ExecStatus 'running' → RoleStatus 'working' (not MessageStatus 'streaming')
721
+ statuses[exec.roleId] = exec.status === 'running' ? 'working'
722
+ : exec.status === 'awaiting_input' ? 'awaiting_input'
723
+ : 'done';
687
724
  }
688
725
 
689
726
  const activeExecutions = activeExecs.map((e) => ({
@@ -1,9 +1,12 @@
1
1
  import { Router, Request, Response, NextFunction } from 'express';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { COMPANY_ROOT } from '../services/file-reader.js';
4
+ import { resolveCodeRoot } from '../services/company-config.js';
4
5
 
5
6
  export const gitRouter = Router();
6
7
 
8
+ type RepoType = 'akb' | 'code';
9
+
7
10
  interface WorktreeInfo {
8
11
  path: string;
9
12
  branch: string;
@@ -17,17 +20,42 @@ interface LastCommit {
17
20
  date: string;
18
21
  }
19
22
 
20
- function git(cmd: string): string {
21
- return execSync(`git ${cmd}`, { cwd: COMPANY_ROOT, encoding: 'utf-8', timeout: 5000 }).trim();
23
+ /**
24
+ * Resolve repository root based on repo type
25
+ */
26
+ function resolveRepoRoot(repo: RepoType = 'akb'): string {
27
+ if (repo === 'akb') {
28
+ return COMPANY_ROOT;
29
+ }
30
+ return resolveCodeRoot(COMPANY_ROOT);
22
31
  }
23
32
 
24
- // GET /api/git/status
25
- gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
33
+ function git(cmd: string, cwd: string): string {
34
+ return execSync(`git ${cmd}`, { cwd, encoding: 'utf-8', timeout: 5000 }).trim();
35
+ }
36
+
37
+ // GET /api/git/status?repo=akb|code
38
+ gitRouter.get('/status', (req: Request, res: Response, next: NextFunction) => {
26
39
  try {
40
+ const repoParam = (req.query.repo as string) ?? 'akb';
41
+ const repo: RepoType = repoParam === 'code' ? 'code' : 'akb';
42
+
43
+ let repoRoot: string;
44
+ try {
45
+ repoRoot = resolveRepoRoot(repo);
46
+ } catch (err) {
47
+ res.status(400).json({
48
+ error: repo === 'code'
49
+ ? 'Code root not configured or inaccessible'
50
+ : 'Company root not found'
51
+ });
52
+ return;
53
+ }
54
+
27
55
  // Current branch
28
56
  let currentBranch: string;
29
57
  try {
30
- currentBranch = git('rev-parse --abbrev-ref HEAD');
58
+ currentBranch = git('rev-parse --abbrev-ref HEAD', repoRoot);
31
59
  } catch {
32
60
  res.status(500).json({ error: 'Not a git repository or git is not available' });
33
61
  return;
@@ -36,7 +64,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
36
64
  // Worktrees
37
65
  let worktrees: WorktreeInfo[] = [];
38
66
  try {
39
- const raw = git('worktree list --porcelain');
67
+ const raw = git('worktree list --porcelain', repoRoot);
40
68
  const blocks = raw.split('\n\n').filter(Boolean);
41
69
  for (const block of blocks) {
42
70
  const lines = block.split('\n');
@@ -44,13 +72,13 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
44
72
  const commitHash = lines.find(l => l.startsWith('HEAD '))?.replace('HEAD ', '') ?? '';
45
73
  const branchLine = lines.find(l => l.startsWith('branch '));
46
74
  const branch = branchLine ? branchLine.replace('branch refs/heads/', '') : '(detached)';
47
- const isMain = lines.some(l => l === 'worktree ' + COMPANY_ROOT) ||
75
+ const isMain = lines.some(l => l === 'worktree ' + repoRoot) ||
48
76
  (!branchLine && lines.some(l => l === 'bare'));
49
77
  worktrees.push({
50
78
  path: wtPath,
51
79
  branch,
52
80
  commitHash,
53
- isMain: wtPath === COMPANY_ROOT,
81
+ isMain: wtPath === repoRoot,
54
82
  });
55
83
  }
56
84
  } catch {
@@ -60,7 +88,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
60
88
  // Stale (unmerged) branches
61
89
  let staleBranches: string[] = [];
62
90
  try {
63
- const raw = git('branch --no-merged develop');
91
+ const raw = git('branch --no-merged develop', repoRoot);
64
92
  staleBranches = raw
65
93
  .split('\n')
66
94
  .map(b => b.trim().replace(/^\*\s*/, ''))
@@ -72,7 +100,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
72
100
  // Unsaved changes count
73
101
  let unsavedChanges = 0;
74
102
  try {
75
- const raw = git('status --porcelain');
103
+ const raw = git('status --porcelain', repoRoot);
76
104
  unsavedChanges = raw ? raw.split('\n').filter(Boolean).length : 0;
77
105
  } catch {
78
106
  unsavedChanges = 0;
@@ -81,7 +109,7 @@ gitRouter.get('/status', (_req: Request, res: Response, next: NextFunction) => {
81
109
  // Last commit
82
110
  let lastCommit: LastCommit | null = null;
83
111
  try {
84
- const raw = git('log -1 --format=%H%n%s%n%aI');
112
+ const raw = git('log -1 --format=%H%n%s%n%aI', repoRoot);
85
113
  const [hash, message, date] = raw.split('\n');
86
114
  if (hash) {
87
115
  lastCommit = { hash, message: message ?? '', date: date ?? '' };
@@ -113,10 +141,10 @@ gitRouter.delete('/worktree/{*path}', (req: Request, res: Response, next: NextFu
113
141
  }
114
142
 
115
143
  try {
116
- git(`worktree remove ${JSON.stringify(worktreePath)}`);
144
+ git(`worktree remove ${JSON.stringify(worktreePath)}`, COMPANY_ROOT);
117
145
  } catch {
118
146
  // Try force remove if normal remove fails
119
- git(`worktree remove --force ${JSON.stringify(worktreePath)}`);
147
+ git(`worktree remove --force ${JSON.stringify(worktreePath)}`, COMPANY_ROOT);
120
148
  }
121
149
 
122
150
  res.json({ success: true, removed: worktreePath });
@@ -146,7 +174,7 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
146
174
 
147
175
  // Delete local branch
148
176
  try {
149
- git(`branch -d ${JSON.stringify(branchName)}`);
177
+ git(`branch -d ${JSON.stringify(branchName)}`, COMPANY_ROOT);
150
178
  } catch (err) {
151
179
  const msg = err instanceof Error ? err.message : 'Unknown error';
152
180
  // If branch is not fully merged, report but continue to try remote
@@ -159,7 +187,7 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
159
187
 
160
188
  // Delete remote branch
161
189
  try {
162
- git(`push origin --delete ${JSON.stringify(branchName)}`);
190
+ git(`push origin --delete ${JSON.stringify(branchName)}`, COMPANY_ROOT);
163
191
  } catch (err) {
164
192
  const msg = err instanceof Error ? err.message : 'Unknown error';
165
193
  if (!msg.includes('remote ref does not exist')) {
@@ -32,9 +32,15 @@ saveRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
32
32
  const result = gitSave(COMPANY_ROOT, message, getRepo(req));
33
33
  res.json({ ok: true, ...result });
34
34
  } catch (err) {
35
- if (err instanceof Error && err.message === 'No changes to save') {
36
- res.status(400).json({ error: err.message });
37
- return;
35
+ if (err instanceof Error) {
36
+ if (err.message === 'No changes to save') {
37
+ res.status(400).json({ error: err.message });
38
+ return;
39
+ }
40
+ if (err.message.includes('Not a git repository') || err.message.includes('codeRoot')) {
41
+ res.status(400).json({ error: 'Repository not initialized. Run git init first.' });
42
+ return;
43
+ }
38
44
  }
39
45
  next(err);
40
46
  }
@@ -64,7 +70,7 @@ saveRouter.post('/init', (req: Request, res: Response, next: NextFunction) => {
64
70
  }
65
71
  });
66
72
 
67
- // POST /api/save/restore
73
+ // POST /api/save/restore?repo=akb|code
68
74
  saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) => {
69
75
  try {
70
76
  const { sha, paths } = req.body ?? {};
@@ -72,9 +78,13 @@ saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) =>
72
78
  res.status(400).json({ error: 'sha is required' });
73
79
  return;
74
80
  }
75
- const result = gitRestore(COMPANY_ROOT, sha, paths);
81
+ const result = gitRestore(COMPANY_ROOT, sha, paths, getRepo(req));
76
82
  res.json({ ok: true, ...result });
77
83
  } catch (err) {
84
+ if (err instanceof Error && (err.message.includes('Not a git repository') || err.message.includes('codeRoot'))) {
85
+ res.status(400).json({ error: 'Repository not initialized. Run git init first.' });
86
+ return;
87
+ }
78
88
  next(err);
79
89
  }
80
90
  });