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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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 ' +
|
|
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 ===
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
});
|