tycono 0.1.42 → 0.1.43

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.42",
3
+ "version": "0.1.43",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -245,96 +245,201 @@ export async function runAgentLoop(config: AgentConfig): Promise<AgentResult> {
245
245
  onTurnComplete?.(turns);
246
246
  }
247
247
 
248
- // ── Verification turn: auto-inject for engineer/cto at depth 0 with write access ──
249
- const verifiableRoles = ['engineer', 'cto'];
250
- if (verifiableRoles.includes(roleId) && !readOnly && depth === 0 && turns > 0) {
251
- const hasFileChanges = allToolCalls.some((tc) =>
252
- ['write', 'edit', 'bash'].includes(tc.name.toLowerCase()),
253
- );
254
-
255
- if (hasFileChanges) {
256
- const verifyPrompt = [
257
- '[AUTO-VERIFICATION] 작업이 완료되었습니다. 아래 검증을 수행하세요:',
258
- '1. `cd src/api && npx tsc --noEmit` — 타입 에러 확인',
259
- '2. `cd src/web && npx tsc --noEmit` — 프론트엔드 타입 에러 확인',
260
- '3. UI/CSS 변경이 있었다면 Playwright MCP로 스크린샷을 촬영하여 시각 검증',
261
- '검증 결과를 간단히 보고하세요.',
248
+ // ── Post-execution phases (depth 0 only) ──
249
+ if (!readOnly && depth === 0 && turns > 0) {
250
+ const node = orgTree.nodes.get(roleId);
251
+ const isCLevel = node?.level === 'c-level';
252
+
253
+ // Phase A: C-Level Supervision Loop — review dispatches, update knowledge, dispatch next
254
+ if (isCLevel && dispatches.length > 0) {
255
+ const dispatchSummary = dispatches.map((d, i) =>
256
+ `${i + 1}. **${d.roleId}**: "${d.task.slice(0, 80)}"\n Result: ${d.result.slice(0, 300)}`,
257
+ ).join('\n\n');
258
+
259
+ const supervisionPrompt = [
260
+ '[SUPERVISION LOOP] Your subordinates have completed their tasks. Follow the C-Level Protocol:',
261
+ '',
262
+ '## Subordinate Results',
263
+ dispatchSummary,
264
+ '',
265
+ '## Required Actions (do ALL of these):',
266
+ '',
267
+ '### 1. Review',
268
+ 'Does each result meet the acceptance criteria? If not, re-dispatch with specific feedback.',
269
+ '',
270
+ '### 2. Knowledge Update (The Loop Step ④)',
271
+ 'Record any new decisions, findings, or analysis in appropriate AKB documents:',
272
+ '- Update your journal (`roles/' + roleId + '/journal/`)',
273
+ '- Update relevant project docs if needed',
274
+ '- Update knowledge/ if there are reusable insights',
275
+ '',
276
+ '### 3. Task Update (The Loop Step ⑤)',
277
+ 'Update task status in the relevant tasks.md or project documents.',
278
+ 'Mark completed items as DONE. Identify the NEXT task to dispatch.',
279
+ '',
280
+ '### 4. Next Dispatch',
281
+ 'If there are remaining tasks (e.g., QA after Engineer, or the next task in the backlog):',
282
+ '- Dispatch the next task to the appropriate subordinate',
283
+ '- If all work is done, synthesize a final report for your superior',
284
+ '',
285
+ 'Execute these actions now using your tools (Read, Edit, Bash, dispatch).',
262
286
  ].join('\n');
263
287
 
264
- messages.push({ role: 'user', content: verifyPrompt });
265
-
266
- // Run one verification turn
267
- if (turns < maxTurns) {
288
+ // Run supervision loop (up to 3 additional rounds of tool use)
289
+ messages.push({ role: 'user', content: supervisionPrompt });
290
+ const maxSupervisionRounds = 3;
291
+ for (let round = 0; round < maxSupervisionRounds && turns < maxTurns; round++) {
292
+ if (abortSignal?.aborted) break;
268
293
  turns++;
269
- const verifyResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
270
- totalInput += verifyResponse.usage.inputTokens;
271
- totalOutput += verifyResponse.usage.outputTokens;
294
+
295
+ const supResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
296
+ totalInput += supResponse.usage.inputTokens;
297
+ totalOutput += supResponse.usage.outputTokens;
272
298
  config.tokenLedger?.record({
273
299
  ts: new Date().toISOString(),
274
300
  jobId: config.jobId ?? 'unknown',
275
301
  roleId,
276
302
  model: config.model ?? 'unknown',
277
- inputTokens: verifyResponse.usage.inputTokens,
278
- outputTokens: verifyResponse.usage.outputTokens,
303
+ inputTokens: supResponse.usage.inputTokens,
304
+ outputTokens: supResponse.usage.outputTokens,
279
305
  });
280
306
 
281
- messages.push({ role: 'assistant', content: verifyResponse.content });
282
-
283
- for (const block of verifyResponse.content) {
307
+ messages.push({ role: 'assistant', content: supResponse.content });
308
+ for (const block of supResponse.content) {
284
309
  if (block.type === 'text' && block.text) {
285
310
  outputParts.push(block.text);
286
311
  onText?.(block.text);
287
312
  }
288
313
  }
289
314
 
290
- // If verification needs tool calls, execute them
291
- if (verifyResponse.stopReason === 'tool_use') {
292
- const verifyToolCalls = verifyResponse.content.filter(
293
- (b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
315
+ // If no tool calls, supervision is done
316
+ if (supResponse.stopReason !== 'tool_use') break;
317
+
318
+ // Execute tool calls
319
+ const supToolCalls = supResponse.content.filter(
320
+ (b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
321
+ );
322
+ const supResults: ToolResult[] = [];
323
+ for (const tc of supToolCalls) {
324
+ allToolCalls.push({ name: tc.name, input: tc.input });
325
+ const result = await executeTool(
326
+ { id: tc.id, name: tc.name, input: tc.input },
327
+ toolExecOptions,
294
328
  );
295
- const verifyResults: ToolResult[] = [];
296
- for (const tc of verifyToolCalls) {
297
- allToolCalls.push({ name: tc.name, input: tc.input });
298
- const result = await executeTool(
299
- { id: tc.id, name: tc.name, input: tc.input },
300
- toolExecOptions,
301
- );
302
- verifyResults.push(result);
329
+ supResults.push(result);
330
+
331
+ // Track additional dispatches from supervision
332
+ if (tc.name === 'dispatch' && !result.is_error) {
333
+ dispatches.push({
334
+ roleId: String(tc.input.roleId),
335
+ task: String(tc.input.task),
336
+ result: result.content,
337
+ });
303
338
  }
304
- // Feed results back for final summary
305
- messages.push({
306
- role: 'user',
307
- content: verifyResults.map((r) => ({
308
- type: 'tool_result' as const,
309
- tool_use_id: r.tool_use_id,
310
- content: r.content,
311
- is_error: r.is_error,
312
- })) as unknown as MessageContent[],
339
+ }
340
+
341
+ messages.push({
342
+ role: 'user',
343
+ content: supResults.map((r) => ({
344
+ type: 'tool_result' as const,
345
+ tool_use_id: r.tool_use_id,
346
+ content: r.content,
347
+ is_error: r.is_error,
348
+ })) as unknown as MessageContent[],
349
+ });
350
+
351
+ onTurnComplete?.(turns);
352
+ }
353
+ }
354
+
355
+ // Phase B: Engineer/CTO Verification — type checking + visual verification
356
+ const verifiableRoles = ['engineer', 'cto'];
357
+ if (verifiableRoles.includes(roleId)) {
358
+ const hasFileChanges = allToolCalls.some((tc) =>
359
+ ['write', 'edit', 'bash'].includes(tc.name.toLowerCase()),
360
+ );
361
+
362
+ if (hasFileChanges) {
363
+ const verifyPrompt = [
364
+ '[AUTO-VERIFICATION] 작업이 완료되었습니다. 아래 검증을 수행하세요:',
365
+ '1. `cd src/api && npx tsc --noEmit` — 타입 에러 확인',
366
+ '2. `cd src/web && npx tsc --noEmit` — 프론트엔드 타입 에러 확인',
367
+ '3. UI/CSS 변경이 있었다면 Playwright MCP로 스크린샷을 촬영하여 시각 검증',
368
+ '검증 결과를 간단히 보고하세요.',
369
+ ].join('\n');
370
+
371
+ messages.push({ role: 'user', content: verifyPrompt });
372
+
373
+ if (turns < maxTurns) {
374
+ turns++;
375
+ const verifyResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
376
+ totalInput += verifyResponse.usage.inputTokens;
377
+ totalOutput += verifyResponse.usage.outputTokens;
378
+ config.tokenLedger?.record({
379
+ ts: new Date().toISOString(),
380
+ jobId: config.jobId ?? 'unknown',
381
+ roleId,
382
+ model: config.model ?? 'unknown',
383
+ inputTokens: verifyResponse.usage.inputTokens,
384
+ outputTokens: verifyResponse.usage.outputTokens,
313
385
  });
314
386
 
315
- if (turns < maxTurns) {
316
- turns++;
317
- const summaryResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
318
- totalInput += summaryResponse.usage.inputTokens;
319
- totalOutput += summaryResponse.usage.outputTokens;
320
- config.tokenLedger?.record({
321
- ts: new Date().toISOString(),
322
- jobId: config.jobId ?? 'unknown',
323
- roleId,
324
- model: config.model ?? 'unknown',
325
- inputTokens: summaryResponse.usage.inputTokens,
326
- outputTokens: summaryResponse.usage.outputTokens,
387
+ messages.push({ role: 'assistant', content: verifyResponse.content });
388
+ for (const block of verifyResponse.content) {
389
+ if (block.type === 'text' && block.text) {
390
+ outputParts.push(block.text);
391
+ onText?.(block.text);
392
+ }
393
+ }
394
+
395
+ // Execute verification tool calls if needed
396
+ if (verifyResponse.stopReason === 'tool_use') {
397
+ const verifyToolCalls = verifyResponse.content.filter(
398
+ (b): b is MessageContent & { type: 'tool_use' } => b.type === 'tool_use',
399
+ );
400
+ const verifyResults: ToolResult[] = [];
401
+ for (const tc of verifyToolCalls) {
402
+ allToolCalls.push({ name: tc.name, input: tc.input });
403
+ const result = await executeTool(
404
+ { id: tc.id, name: tc.name, input: tc.input },
405
+ toolExecOptions,
406
+ );
407
+ verifyResults.push(result);
408
+ }
409
+ messages.push({
410
+ role: 'user',
411
+ content: verifyResults.map((r) => ({
412
+ type: 'tool_result' as const,
413
+ tool_use_id: r.tool_use_id,
414
+ content: r.content,
415
+ is_error: r.is_error,
416
+ })) as unknown as MessageContent[],
327
417
  });
328
- for (const block of summaryResponse.content) {
329
- if (block.type === 'text' && block.text) {
330
- outputParts.push(block.text);
331
- onText?.(block.text);
418
+
419
+ if (turns < maxTurns) {
420
+ turns++;
421
+ const summaryResponse = await llm.chat(context.systemPrompt, messages, tools, abortSignal);
422
+ totalInput += summaryResponse.usage.inputTokens;
423
+ totalOutput += summaryResponse.usage.outputTokens;
424
+ config.tokenLedger?.record({
425
+ ts: new Date().toISOString(),
426
+ jobId: config.jobId ?? 'unknown',
427
+ roleId,
428
+ model: config.model ?? 'unknown',
429
+ inputTokens: summaryResponse.usage.inputTokens,
430
+ outputTokens: summaryResponse.usage.outputTokens,
431
+ });
432
+ for (const block of summaryResponse.content) {
433
+ if (block.type === 'text' && block.text) {
434
+ outputParts.push(block.text);
435
+ onText?.(block.text);
436
+ }
332
437
  }
333
438
  }
334
439
  }
335
- }
336
440
 
337
- onTurnComplete?.(turns);
441
+ onTurnComplete?.(turns);
442
+ }
338
443
  }
339
444
  }
340
445
  }
@@ -363,6 +363,9 @@ function loadCeoDecisions(companyRoot: string): string | null {
363
363
  }
364
364
 
365
365
  function buildDispatchSection(orgTree: OrgTree, roleId: string, subordinates: string[], teamStatus?: TeamStatus): string {
366
+ const node = orgTree.nodes.get(roleId);
367
+ const isCLevel = node?.level === 'c-level';
368
+
366
369
  const subInfo = subordinates.map((id) => {
367
370
  const sub = orgTree.nodes.get(id);
368
371
  const base = sub ? `- **${sub.name}** (\`${id}\`): ${sub.persona.split('\n')[0]}` : `- ${id}`;
@@ -376,9 +379,7 @@ function buildDispatchSection(orgTree: OrgTree, roleId: string, subordinates: st
376
379
 
377
380
  const exampleSubId = subordinates[0] ?? 'engineer';
378
381
 
379
- return `# Dispatch (Team Management)
380
-
381
- You can assign tasks to your direct reports. They will execute independently and return results.
382
+ let section = `# Dispatch (Team Management)
382
383
 
383
384
  ## Available Team Members
384
385
  ${subInfo}
@@ -401,21 +402,76 @@ The command will:
401
402
  If the subordinate takes longer than 100s, you'll get a job ID. Check the result with:
402
403
  \`\`\`bash
403
404
  python3 "$DISPATCH_CMD" --check <jobId>
404
- \`\`\`
405
+ \`\`\``;
405
406
 
406
- ## Examples
407
+ // C-level roles get mandatory delegation rules
408
+ if (isCLevel) {
409
+ section += `
407
410
 
408
- \`\`\`bash
409
- # Assign a task and wait for result
410
- python3 "$DISPATCH_CMD" ${exampleSubId} "프로젝트 현황을 확인하고 보고서를 작성해"
411
+ ## C-Level Delegation Protocol (MANDATORY)
412
+
413
+ **You are a MANAGER. You do NOT write code, tests, or implementation yourself.**
414
+ ⛔ **Your job is to PLAN, DELEGATE, REVIEW, and UPDATE KNOWLEDGE.**
415
+
416
+ ### Core Rule: Always Delegate Down
417
+
418
+ When you receive a directive:
419
+ 1. **Analyze** — Break it into sub-tasks appropriate for each subordinate
420
+ 2. **Dispatch** — Assign tasks to subordinates with clear acceptance criteria
421
+ 3. **Monitor** — Wait for results, review quality
422
+ 4. **Follow up** — If output doesn't meet criteria, dispatch back with feedback
423
+ 5. **Report** — Synthesize results and report to your superior
424
+
425
+ ### What You Do vs What Subordinates Do
426
+
427
+ | YOU (C-Level) | SUBORDINATES (Members) |
428
+ |---------------|----------------------|
429
+ | Plan & decompose tasks | Implement code/design/tests |
430
+ | Dispatch with clear specs | Execute and return results |
431
+ | Review output quality | Fix issues when told |
432
+ | Update knowledge & tasks | Update their own journals |
433
+ | Report to superior | Report to you |
411
434
 
412
- # Check a previously dispatched job result
413
- python3 "$DISPATCH_CMD" --check job-xxx-123
435
+ ### The Supervision Loop (CRITICAL)
436
+
437
+ After EVERY dispatch, follow this loop:
438
+
439
+ \`\`\`
440
+ DISPATCH → WAIT → REVIEW → DECIDE
441
+ ├── PASS → Knowledge Update → Task Update → Next Dispatch
442
+ └── FAIL → Re-dispatch with feedback
414
443
  \`\`\`
415
444
 
445
+ 1. **Review**: Does the output meet acceptance criteria?
446
+ 2. **Knowledge Update**: Record decisions, findings, analysis in AKB (journals, knowledge/)
447
+ 3. **Task Update**: Update task status in tasks.md or project docs
448
+ 4. **Next Dispatch**: Identify and dispatch the next task
449
+
450
+ ### Dispatch Quality Requirements
451
+
452
+ Every dispatch MUST include:
453
+ - **Context**: What documents/files to read first (CLAUDE.md + relevant Hub + SKILL.md)
454
+ - **Task**: Specific deliverable with acceptance criteria
455
+ - **Constraints**: File paths, standards, what NOT to do
456
+ - **AKB instruction**: "⛔ AKB Rule: Read CLAUDE.md before starting work."
457
+
458
+ ### Anti-Patterns (NEVER do these)
459
+
460
+ - ❌ Writing code yourself instead of dispatching to engineer
461
+ - ❌ Dispatching without acceptance criteria
462
+ - ❌ Accepting output without reviewing it
463
+ - ❌ Forgetting to update knowledge/tasks after work completes
464
+ - ❌ Doing only 1 dispatch when you should chain multiple (Engineer → QA)
465
+ - ❌ Reporting to superior without synthesizing subordinate outputs`;
466
+ } else {
467
+ section += `
468
+
416
469
  ## Rules
417
470
  - Only dispatch to your direct reports listed above
418
471
  - Include clear task description, acceptance criteria, and relevant file paths
419
472
  - The dispatched agent will work independently and return results to you
420
473
  - After receiving results, synthesize and report back`;
474
+ }
475
+
476
+ return section;
421
477
  }
@@ -277,10 +277,10 @@ knowledgeRouter.get('/{*path}', (req: Request, res: Response, next: NextFunction
277
277
  return;
278
278
  }
279
279
 
280
- const absPath = path.join(knowledgeDir(), docId);
280
+ const absPath = path.resolve(companyRoot(), docId);
281
281
 
282
- // Security: ensure path stays within knowledgeDir
283
- if (!absPath.startsWith(knowledgeDir())) {
282
+ // Security: ensure path stays within companyRoot
283
+ if (!absPath.startsWith(companyRoot() + path.sep) && absPath !== companyRoot()) {
284
284
  res.status(403).json({ error: 'Forbidden' });
285
285
  return;
286
286
  }