tycono 0.1.93-beta.0 → 0.1.93-beta.2
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 +1 -1
- package/src/api/src/engine/agent-loop.ts +78 -0
- package/src/api/src/engine/runners/claude-cli.ts +12 -2
- package/src/api/src/routes/execute.ts +100 -4
- package/src/api/src/routes/git.ts +47 -15
- package/src/api/src/routes/save.ts +15 -5
- package/src/api/src/services/execution-manager.ts +8 -2
- package/src/api/src/services/wave-multiplexer.ts +14 -1
- package/src/web/dist/assets/{index-BLB8Scqo.js → index-BoJAXuTo.js} +48 -48
- package/src/web/dist/assets/{preview-app-DJl5kOhT.js → preview-app-CywDONM1.js} +1 -1
- package/src/web/dist/index.html +1 -1
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
|
}
|
|
@@ -6,6 +6,7 @@ import { assembleContext } from '../context-assembler.js';
|
|
|
6
6
|
import { getSubordinates } from '../org-tree.js';
|
|
7
7
|
import { readConfig, resolveCodeRoot } from '../../services/company-config.js';
|
|
8
8
|
import { getTokenLedger } from '../../services/token-ledger.js';
|
|
9
|
+
import { getSession } from '../../services/session-store.js';
|
|
9
10
|
import type { ExecutionRunner, RunnerConfig, RunnerCallbacks, RunnerHandle, RunnerResult } from './types.js';
|
|
10
11
|
|
|
11
12
|
/* ─── Dispatch Bridge Script (Python3) ────── */
|
|
@@ -65,13 +66,17 @@ def get_status(job_id):
|
|
|
65
66
|
def start_job(role_id, task):
|
|
66
67
|
parent_session = os.environ.get('DISPATCH_PARENT_SESSION', os.environ.get('DISPATCH_PARENT_JOB', ''))
|
|
67
68
|
source_role = os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo')
|
|
68
|
-
|
|
69
|
+
wave_id = os.environ.get('DISPATCH_WAVE_ID', '')
|
|
70
|
+
payload = {
|
|
69
71
|
'type': 'assign',
|
|
70
72
|
'roleId': role_id,
|
|
71
73
|
'task': task,
|
|
72
74
|
'sourceRole': source_role,
|
|
73
75
|
'parentSessionId': parent_session if parent_session else None,
|
|
74
|
-
|
|
76
|
+
}
|
|
77
|
+
if wave_id:
|
|
78
|
+
payload['waveId'] = wave_id
|
|
79
|
+
body = json.dumps(payload).encode()
|
|
75
80
|
req = urllib.request.Request(f'{api}/api/jobs', body, {'Content-Type': 'application/json'})
|
|
76
81
|
resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
|
77
82
|
return resp['jobId']
|
|
@@ -347,6 +352,11 @@ export class ClaudeCliRunner implements ExecutionRunner {
|
|
|
347
352
|
cleanEnv.DISPATCH_SUBORDINATES = subordinates.join(', ');
|
|
348
353
|
cleanEnv.DISPATCH_PARENT_SESSION = config.sessionId;
|
|
349
354
|
cleanEnv.DISPATCH_PARENT_JOB = config.sessionId; // deprecated, kept for backward compat
|
|
355
|
+
// BUG-W02 fix: propagate waveId to child dispatches via env
|
|
356
|
+
if (config.sessionId) {
|
|
357
|
+
const parentSes = getSession(config.sessionId);
|
|
358
|
+
if (parentSes?.waveId) cleanEnv.DISPATCH_WAVE_ID = parentSes.waveId;
|
|
359
|
+
}
|
|
350
360
|
// dispatch 명령어 경로를 PATH에 추가하지 않고 절대 경로로 사용
|
|
351
361
|
cleanEnv.DISPATCH_CMD = dispatchScript;
|
|
352
362
|
cleanEnv.CONSULT_CMD = consultScript;
|
|
@@ -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,28 @@ 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
|
+
// BUG-W03 fix: register ALL wave sessions (including completed) for tree display
|
|
64
|
+
for (const [wid, sids] of waveGroups) {
|
|
65
|
+
for (const sid of sids) {
|
|
66
|
+
// getActiveExecution falls back to stream recovery for completed sessions
|
|
67
|
+
const exec = executionManager.getActiveExecution(sid);
|
|
68
|
+
if (exec) {
|
|
69
|
+
waveMultiplexer.registerSession(wid, exec);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
51
74
|
jsonResponse(res, 200, { waves: waveMultiplexer.getActiveWaves() });
|
|
52
75
|
return;
|
|
53
76
|
}
|
|
@@ -253,6 +276,7 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
253
276
|
const session = createSession(roleId, {
|
|
254
277
|
mode: readOnly ? 'talk' : 'do',
|
|
255
278
|
source: parentSessionId ? 'dispatch' : sessionSource,
|
|
279
|
+
...(parentSessionId && { parentSessionId }),
|
|
256
280
|
...(waveId && { waveId }),
|
|
257
281
|
});
|
|
258
282
|
const sessionId = session.id;
|
|
@@ -301,9 +325,18 @@ function handleStartJob(body: Record<string, unknown>, res: ServerResponse): voi
|
|
|
301
325
|
|
|
302
326
|
function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): void {
|
|
303
327
|
const directive = body.directive as string;
|
|
304
|
-
|
|
328
|
+
let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
|
|
305
329
|
const waveId = body.waveId as string | undefined;
|
|
306
330
|
|
|
331
|
+
// BUG-W01 fix: auto-collect sessionIds from session-store when waveId is present
|
|
332
|
+
if (waveId && (!sessionIds || sessionIds.length === 0)) {
|
|
333
|
+
const allSessions = listSessions();
|
|
334
|
+
sessionIds = allSessions
|
|
335
|
+
.filter(s => s.waveId === waveId)
|
|
336
|
+
.map(s => s.id);
|
|
337
|
+
console.log(`[WaveSave] Auto-collected ${sessionIds.length} sessionIds for wave ${waveId}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
307
340
|
if (!directive || !sessionIds || sessionIds.length === 0) {
|
|
308
341
|
jsonResponse(res, 400, { error: 'directive and sessionIds are required' });
|
|
309
342
|
return;
|
|
@@ -399,7 +432,23 @@ function handleWaveStream(waveId: string, url: string, res: ServerResponse, req:
|
|
|
399
432
|
const fromMatch = url.match(/[?&]from=(\d+)/);
|
|
400
433
|
const fromWaveSeq = fromMatch ? parseInt(fromMatch[1], 10) : 0;
|
|
401
434
|
|
|
402
|
-
|
|
435
|
+
let sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
436
|
+
|
|
437
|
+
// Recovery: if wave→session mapping was lost (e.g. server restart),
|
|
438
|
+
// rebuild from session-store (sessions have waveId) + executionManager
|
|
439
|
+
if (sessionIds.length === 0) {
|
|
440
|
+
const allSessions = listSessions();
|
|
441
|
+
const waveSessions = allSessions.filter(s => s.waveId === waveId);
|
|
442
|
+
for (const ses of waveSessions) {
|
|
443
|
+
// getActiveExecution recovers from stream files for completed sessions too
|
|
444
|
+
const exec = executionManager.getActiveExecution(ses.id);
|
|
445
|
+
if (exec) {
|
|
446
|
+
waveMultiplexer.registerSession(waveId, exec);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
sessionIds = waveMultiplexer.getWaveSessionIds(waveId);
|
|
450
|
+
}
|
|
451
|
+
|
|
403
452
|
if (sessionIds.length === 0) {
|
|
404
453
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
405
454
|
res.end(JSON.stringify({ error: `No sessions found for wave: ${waveId}` }));
|
|
@@ -681,9 +730,56 @@ function handleWave(body: Record<string, unknown>, req: IncomingMessage, res: Se
|
|
|
681
730
|
function handleStatus(res: ServerResponse): void {
|
|
682
731
|
const statuses: Record<string, string> = {};
|
|
683
732
|
|
|
684
|
-
|
|
733
|
+
let activeExecs = executionManager.listExecutions({ active: true });
|
|
734
|
+
|
|
735
|
+
// Recovery: if in-memory map is empty (e.g. after server restart),
|
|
736
|
+
// rebuild active executions from persisted session-store + activity-streams
|
|
737
|
+
if (activeExecs.length === 0) {
|
|
738
|
+
const allSessions = listSessions();
|
|
739
|
+
const activeSessions = allSessions.filter(s =>
|
|
740
|
+
s.status === 'active' &&
|
|
741
|
+
(s.source === 'wave' || s.source === 'dispatch' || s.source === 'chat')
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
const recovered: typeof activeExecs = [];
|
|
745
|
+
for (const ses of activeSessions) {
|
|
746
|
+
// Check activity-stream for actual running state
|
|
747
|
+
if (!ActivityStream.exists(ses.id)) continue;
|
|
748
|
+
const events = ActivityStream.readFrom(ses.id, 0);
|
|
749
|
+
if (events.length === 0) continue;
|
|
750
|
+
|
|
751
|
+
const startEvent = events.find(e => e.type === 'msg:start');
|
|
752
|
+
if (!startEvent) continue;
|
|
753
|
+
|
|
754
|
+
const doneEvent = events.find(e => e.type === 'msg:done');
|
|
755
|
+
const errorEvent = events.find(e => e.type === 'msg:error');
|
|
756
|
+
|
|
757
|
+
// Only include sessions that haven't finished yet
|
|
758
|
+
if (doneEvent || errorEvent) continue;
|
|
759
|
+
|
|
760
|
+
const task = (startEvent.data?.task as string) ?? ses.title ?? '';
|
|
761
|
+
recovered.push({
|
|
762
|
+
id: `recovered-${ses.id}`,
|
|
763
|
+
type: (startEvent.data?.type as string ?? 'assign') as 'assign' | 'wave' | 'consult',
|
|
764
|
+
roleId: ses.roleId,
|
|
765
|
+
task,
|
|
766
|
+
status: 'running',
|
|
767
|
+
childSessionIds: [],
|
|
768
|
+
createdAt: ses.createdAt,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (recovered.length > 0) {
|
|
773
|
+
activeExecs = recovered;
|
|
774
|
+
console.log(`[ExecStatus] Recovered ${recovered.length} active executions from session-store`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
685
778
|
for (const exec of activeExecs) {
|
|
686
|
-
|
|
779
|
+
// ExecStatus 'running' → RoleStatus 'working' (not MessageStatus 'streaming')
|
|
780
|
+
statuses[exec.roleId] = exec.status === 'running' ? 'working'
|
|
781
|
+
: exec.status === 'awaiting_input' ? 'awaiting_input'
|
|
782
|
+
: 'done';
|
|
687
783
|
}
|
|
688
784
|
|
|
689
785
|
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,12 @@ gitRouter.delete('/worktree/{*path}', (req: Request, res: Response, next: NextFu
|
|
|
113
141
|
}
|
|
114
142
|
|
|
115
143
|
try {
|
|
116
|
-
|
|
144
|
+
const repoRoot = resolveRepoRoot('code');
|
|
145
|
+
git(`worktree remove ${JSON.stringify(worktreePath)}`, repoRoot);
|
|
117
146
|
} catch {
|
|
118
147
|
// Try force remove if normal remove fails
|
|
119
|
-
|
|
148
|
+
const repoRoot = resolveRepoRoot('code');
|
|
149
|
+
git(`worktree remove --force ${JSON.stringify(worktreePath)}`, repoRoot);
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
res.json({ success: true, removed: worktreePath });
|
|
@@ -146,7 +176,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
|
|
|
146
176
|
|
|
147
177
|
// Delete local branch
|
|
148
178
|
try {
|
|
149
|
-
|
|
179
|
+
const repoRoot = resolveRepoRoot('code');
|
|
180
|
+
git(`branch -d ${JSON.stringify(branchName)}`, repoRoot);
|
|
150
181
|
} catch (err) {
|
|
151
182
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
152
183
|
// If branch is not fully merged, report but continue to try remote
|
|
@@ -159,7 +190,8 @@ gitRouter.delete('/branch/{*name}', (req: Request, res: Response, next: NextFunc
|
|
|
159
190
|
|
|
160
191
|
// Delete remote branch
|
|
161
192
|
try {
|
|
162
|
-
|
|
193
|
+
const repoRoot = resolveRepoRoot('code');
|
|
194
|
+
git(`push origin --delete ${JSON.stringify(branchName)}`, repoRoot);
|
|
163
195
|
} catch (err) {
|
|
164
196
|
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
165
197
|
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
|
});
|
|
@@ -287,9 +287,15 @@ class ExecutionManager {
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
// BUG-W02 fix: propagate waveId from parent session to child
|
|
291
|
+
const parentSession = getSession(execution.sessionId);
|
|
292
|
+
const parentWaveId = parentSession?.waveId;
|
|
293
|
+
|
|
290
294
|
const childSession = createSession(subRoleId, {
|
|
291
295
|
mode: 'do',
|
|
292
296
|
source: 'dispatch',
|
|
297
|
+
parentSessionId: execution.sessionId,
|
|
298
|
+
...(parentWaveId && { waveId: parentWaveId }),
|
|
293
299
|
});
|
|
294
300
|
const dispatchMsg: Message = {
|
|
295
301
|
id: `msg-${Date.now()}-dispatch-${subRoleId}`,
|
|
@@ -761,7 +767,7 @@ class ExecutionManager {
|
|
|
761
767
|
const status: ExecStatus = awaitingEvent && !doneEvent ? 'awaiting_input'
|
|
762
768
|
: doneEvent ? 'done'
|
|
763
769
|
: errorEvent ? 'error'
|
|
764
|
-
: '
|
|
770
|
+
: 'running'; // No done/error event = still running
|
|
765
771
|
|
|
766
772
|
const candidate = {
|
|
767
773
|
streamId,
|
|
@@ -816,7 +822,7 @@ class ExecutionManager {
|
|
|
816
822
|
const status: ExecStatus = awaitingEvent && !doneEvent ? 'awaiting_input'
|
|
817
823
|
: doneEvent ? 'done'
|
|
818
824
|
: errorEvent ? 'error'
|
|
819
|
-
: '
|
|
825
|
+
: 'running'; // No done/error event = still running
|
|
820
826
|
|
|
821
827
|
const stream = ActivityStream.getOrCreate(sessionId, startEvent.roleId);
|
|
822
828
|
const execution: Execution = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ActivityStream, type ActivityEvent, type ActivitySubscriber } from './activity-stream.js';
|
|
2
2
|
import type { Execution } from './execution-manager.js';
|
|
3
|
+
import { getSession } from './session-store.js';
|
|
3
4
|
import type { Response } from 'express';
|
|
4
5
|
|
|
5
6
|
/* ─── Types ──────────────────────────────── */
|
|
@@ -181,7 +182,19 @@ class WaveMultiplexer {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
onExecutionCreated(execution: Execution): void {
|
|
184
|
-
|
|
185
|
+
// Check multiplexer's in-memory map first
|
|
186
|
+
let waveId = this.findWaveIdForSession(execution.sessionId) ?? this.findWaveIdForSession(execution.parentSessionId ?? '');
|
|
187
|
+
|
|
188
|
+
// BUG-W02 fix: also check session-store for waveId (propagated from parent)
|
|
189
|
+
if (!waveId) {
|
|
190
|
+
const session = getSession(execution.sessionId);
|
|
191
|
+
if (session?.waveId) waveId = session.waveId;
|
|
192
|
+
}
|
|
193
|
+
if (!waveId && execution.parentSessionId) {
|
|
194
|
+
const parentSession = getSession(execution.parentSessionId);
|
|
195
|
+
if (parentSession?.waveId) waveId = parentSession.waveId;
|
|
196
|
+
}
|
|
197
|
+
|
|
185
198
|
if (!waveId) return;
|
|
186
199
|
|
|
187
200
|
this.registerSession(waveId, execution);
|