tycono 0.1.59 → 0.1.61
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/create-server.ts +2 -0
- package/src/api/src/engine/context-assembler.ts +11 -0
- package/src/api/src/routes/quests.ts +41 -0
- package/src/api/src/routes/speech.ts +54 -3
- package/src/api/src/services/job-manager.ts +21 -1
- package/src/web/dist/assets/index-Bx4F8eya.js +101 -0
- package/src/web/dist/assets/index-CdkeNB5B.css +1 -0
- package/src/web/dist/assets/{preview-app-Dxb6NBei.js → preview-app-C0OnJLc1.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-C-K3xYmU.css +0 -1
- package/src/web/dist/assets/index-Jayn_T3T.js +0 -101
package/package.json
CHANGED
|
@@ -29,6 +29,7 @@ import { costRouter } from './routes/cost.js';
|
|
|
29
29
|
import { syncRouter } from './routes/sync.js';
|
|
30
30
|
import { gitRouter } from './routes/git.js';
|
|
31
31
|
import { skillsRouter } from './routes/skills.js';
|
|
32
|
+
import { questsRouter } from './routes/quests.js';
|
|
32
33
|
import { importKnowledge } from './services/knowledge-importer.js';
|
|
33
34
|
import { AnthropicProvider, type LLMProvider } from './engine/llm-adapter.js';
|
|
34
35
|
import { readConfig } from './services/company-config.js';
|
|
@@ -195,6 +196,7 @@ export function createExpressApp(): express.Application {
|
|
|
195
196
|
app.use('/api/sync', syncRouter);
|
|
196
197
|
app.use('/api/git', gitRouter);
|
|
197
198
|
app.use('/api/skills', skillsRouter);
|
|
199
|
+
app.use('/api/quests', questsRouter);
|
|
198
200
|
|
|
199
201
|
app.get('/api/health', (_req, res) => {
|
|
200
202
|
res.json({ status: 'ok', companyRoot: COMPANY_ROOT });
|
|
@@ -112,6 +112,17 @@ export function assembleContext(
|
|
|
112
112
|
// Dispatch 도구 안내 (하위 Role이 있는 경우)
|
|
113
113
|
if (subordinates.length > 0) {
|
|
114
114
|
sections.push(buildDispatchSection(orgTree, roleId, subordinates, options?.teamStatus));
|
|
115
|
+
} else if (node.level === 'c-level') {
|
|
116
|
+
// C-level with no subordinates — clarify authority boundaries
|
|
117
|
+
sections.push(`# Team Structure
|
|
118
|
+
|
|
119
|
+
⚠️ **You have no direct reports.** You are an individual contributor at the C-level.
|
|
120
|
+
|
|
121
|
+
- You CANNOT dispatch tasks to other roles (no subordinates)
|
|
122
|
+
- You CAN consult other roles for information (see Consult section below)
|
|
123
|
+
- You MUST do the work yourself — research, analyze, write, decide
|
|
124
|
+
- If implementation requires another role (e.g., engineering work), recommend it to CEO
|
|
125
|
+
- Make decisions within your authority autonomously — do NOT ask CEO for decisions you can make yourself`);
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
// Consult 도구 안내 (상담 가능한 Role이 있는 경우)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
5
|
+
|
|
6
|
+
export const questsRouter = Router();
|
|
7
|
+
|
|
8
|
+
const QUEST_FILE = () => join(COMPANY_ROOT, '.tycono', 'quest-progress.json');
|
|
9
|
+
|
|
10
|
+
function readProgress(): Record<string, unknown> {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(QUEST_FILE(), 'utf-8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return { completedQuests: [], activeChapter: 1, sideQuestsCompleted: [] };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function writeProgress(data: Record<string, unknown>) {
|
|
19
|
+
mkdirSync(join(COMPANY_ROOT, '.tycono'), { recursive: true });
|
|
20
|
+
writeFileSync(QUEST_FILE(), JSON.stringify(data, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// GET /api/quests/progress
|
|
24
|
+
questsRouter.get('/progress', (_req: Request, res: Response, next: NextFunction) => {
|
|
25
|
+
try {
|
|
26
|
+
res.json(readProgress());
|
|
27
|
+
} catch (err) { next(err); }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// PUT /api/quests/progress
|
|
31
|
+
questsRouter.put('/progress', (req: Request, res: Response, next: NextFunction) => {
|
|
32
|
+
try {
|
|
33
|
+
const body = req.body;
|
|
34
|
+
if (!body || typeof body !== 'object') {
|
|
35
|
+
res.status(400).json({ error: 'Invalid body' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
writeProgress(body);
|
|
39
|
+
res.json({ ok: true, ...body });
|
|
40
|
+
} catch (err) { next(err); }
|
|
41
|
+
});
|
|
@@ -214,19 +214,49 @@ function buildCompanyContext(): string {
|
|
|
214
214
|
}
|
|
215
215
|
} catch { /* no org */ }
|
|
216
216
|
|
|
217
|
-
// 3. Active projects
|
|
217
|
+
// 3. Active projects + current phase from tasks.md
|
|
218
218
|
try {
|
|
219
219
|
const projectsContent = readFile('projects/projects.md');
|
|
220
220
|
const rows = parseMarkdownTable(projectsContent);
|
|
221
221
|
const activeProjects = rows
|
|
222
222
|
.filter(r => (r.status ?? r.상태 ?? '').toLowerCase() !== 'archived')
|
|
223
|
-
.map(r =>
|
|
223
|
+
.map(r => {
|
|
224
|
+
const name = r.name ?? r.project ?? r.프로젝트 ?? '';
|
|
225
|
+
const status = r.status ?? r.상태 ?? '';
|
|
226
|
+
const folder = r.folder ?? r.path ?? r.경로 ?? '';
|
|
227
|
+
// Try to read tasks.md for current phase info
|
|
228
|
+
let phaseInfo = '';
|
|
229
|
+
if (folder) {
|
|
230
|
+
try {
|
|
231
|
+
const tasksPath = `${folder.replace(/^\//, '')}/tasks.md`;
|
|
232
|
+
const tasksContent = readFile(tasksPath);
|
|
233
|
+
// Extract current phase (look for "Current" or latest non-done phase)
|
|
234
|
+
const phaseMatch = tasksContent.match(/##\s+(Phase\s+\S+[^\n]*)/gi);
|
|
235
|
+
if (phaseMatch) phaseInfo = ` — ${phaseMatch[0].replace(/^##\s+/, '').slice(0, 60)}`;
|
|
236
|
+
} catch { /* no tasks.md */ }
|
|
237
|
+
}
|
|
238
|
+
return `- ${name} (${status}${phaseInfo})`;
|
|
239
|
+
})
|
|
224
240
|
.slice(0, 5);
|
|
225
241
|
if (activeProjects.length > 0) {
|
|
226
242
|
parts.push(`Active Projects:\n${activeProjects.join('\n')}`);
|
|
227
243
|
}
|
|
228
244
|
} catch { /* no projects */ }
|
|
229
245
|
|
|
246
|
+
// 3b. Tech stack reality check (prevent hallucination about wrong tech)
|
|
247
|
+
try {
|
|
248
|
+
const config = readConfig(COMPANY_ROOT);
|
|
249
|
+
if (config.codeRoot) {
|
|
250
|
+
const pkgPath = path.join(config.codeRoot, 'package.json');
|
|
251
|
+
if (fs.existsSync(pkgPath)) {
|
|
252
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
253
|
+
const name = pkg.name ?? '';
|
|
254
|
+
const version = pkg.version ?? '';
|
|
255
|
+
parts.push(`Tech Stack: ${name}@${version} — TypeScript + React + Node.js (Express). NO Python in codebase. NO ongoing language migration.`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch { /* no package.json */ }
|
|
259
|
+
|
|
230
260
|
// 4. Knowledge highlights (hub TL;DRs, max 3)
|
|
231
261
|
try {
|
|
232
262
|
const knowledgeHub = readFile('knowledge/knowledge.md');
|
|
@@ -353,6 +383,26 @@ function buildRoleContext(roleId: string): string {
|
|
|
353
383
|
}
|
|
354
384
|
} catch { /* no decisions */ }
|
|
355
385
|
|
|
386
|
+
// 5. If sparse context, add architecture/tech-debt highlights as fallback
|
|
387
|
+
if (parts.length < 2) {
|
|
388
|
+
try {
|
|
389
|
+
const techDebtPath = path.join(COMPANY_ROOT, 'architecture', 'tech-debt.md');
|
|
390
|
+
if (fs.existsSync(techDebtPath)) {
|
|
391
|
+
const tdContent = fs.readFileSync(techDebtPath, 'utf-8');
|
|
392
|
+
// Extract active (non-fixed) items
|
|
393
|
+
const rows = parseMarkdownTable(tdContent);
|
|
394
|
+
const active = rows
|
|
395
|
+
.filter(r => !(r.status ?? '').toLowerCase().includes('fixed') && !(r.status ?? '').toLowerCase().includes('done'))
|
|
396
|
+
.slice(0, 3)
|
|
397
|
+
.map(r => `- ${r.id ?? ''}: ${r.title ?? r.issue ?? ''} (${r.status ?? ''})`)
|
|
398
|
+
.filter(s => s.length > 10);
|
|
399
|
+
if (active.length > 0) {
|
|
400
|
+
parts.push(`[Active Tech Debt]\n${active.join('\n')}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch { /* no tech-debt */ }
|
|
404
|
+
}
|
|
405
|
+
|
|
356
406
|
return parts.length > 0
|
|
357
407
|
? `\n\nYOUR KNOWLEDGE (real AKB context — reference this in conversation):\n${parts.join('\n\n')}`
|
|
358
408
|
: '';
|
|
@@ -578,10 +628,11 @@ ${relContext}
|
|
|
578
628
|
${roleStyle}
|
|
579
629
|
|
|
580
630
|
GROUNDING (CRITICAL):
|
|
581
|
-
You have been given real company knowledge above under "YOUR KNOWLEDGE". This is from
|
|
631
|
+
You have been given real company knowledge above under "COMPANY CONTEXT" and "YOUR KNOWLEDGE". This is from AKB files, journal, CEO waves, standups, and decisions.
|
|
582
632
|
You MUST reference this real context in your conversations — mention specific projects, decisions, tasks, or events by name.
|
|
583
633
|
Do NOT generate generic workplace chatter. Every message should show you're aware of what's actually happening in the company.
|
|
584
634
|
If your knowledge section mentions a specific decision or wave, reference it naturally (e.g. "after the test minimization decision..." or "CEO's wave about side panel...").
|
|
635
|
+
NEVER invent or assume technologies, tools, migrations, or projects NOT mentioned in the context above. If the Tech Stack says "TypeScript + React + Node.js", do NOT talk about Python, tc.py, or any language migration. Only discuss what is explicitly in your provided context.
|
|
585
636
|
|
|
586
637
|
CONVERSATION RULES:
|
|
587
638
|
1. Stay deeply in character — your expertise, vocabulary, and concerns should be DISTINCT from other roles.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { COMPANY_ROOT } from './file-reader.js';
|
|
2
2
|
import { ActivityStream, type ActivityEvent } from './activity-stream.js';
|
|
3
3
|
import { buildOrgTree } from '../engine/org-tree.js';
|
|
4
|
+
import { validateDispatch, validateConsult } from '../engine/authority-validator.js';
|
|
4
5
|
import { createRunner } from '../engine/runners/index.js';
|
|
5
6
|
import type { ExecutionRunner } from '../engine/runners/types.js';
|
|
6
7
|
import { setActivity, updateActivity, completeActivity } from './activity-tracker.js';
|
|
@@ -117,11 +118,30 @@ class JobManager {
|
|
|
117
118
|
return () => { this.jobCreatedListeners.delete(listener); };
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
/** Start a new execution job. Returns the Job immediately (fire-and-forget).
|
|
121
|
+
/** Start a new execution job. Returns the Job immediately (fire-and-forget).
|
|
122
|
+
* Throws if sourceRole lacks authority to dispatch/consult the target role. */
|
|
121
123
|
startJob(params: StartJobParams): Job {
|
|
122
124
|
const jobId = `job-${Date.now()}-${this.nextId++}`;
|
|
123
125
|
const orgTree = buildOrgTree(COMPANY_ROOT);
|
|
124
126
|
|
|
127
|
+
// Authority gate: validate dispatch/consult authority at job creation
|
|
128
|
+
if (params.sourceRole && params.sourceRole !== 'ceo') {
|
|
129
|
+
if (params.type === 'consult') {
|
|
130
|
+
const auth = validateConsult(orgTree, params.sourceRole, params.roleId);
|
|
131
|
+
if (!auth.allowed) {
|
|
132
|
+
console.warn(`[JobManager] Authority denied: ${params.sourceRole} → ${params.roleId} (consult): ${auth.reason}`);
|
|
133
|
+
throw new Error(`Authority denied: ${auth.reason}`);
|
|
134
|
+
}
|
|
135
|
+
} else if (params.type === 'assign' && params.parentJobId) {
|
|
136
|
+
// Only validate dispatch authority for child jobs (not CEO waves)
|
|
137
|
+
const auth = validateDispatch(orgTree, params.sourceRole, params.roleId);
|
|
138
|
+
if (!auth.allowed) {
|
|
139
|
+
console.warn(`[JobManager] Authority denied: ${params.sourceRole} → ${params.roleId} (dispatch): ${auth.reason}`);
|
|
140
|
+
throw new Error(`Authority denied: ${auth.reason}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
const stream = new ActivityStream(jobId, params.roleId, params.parentJobId);
|
|
126
146
|
|
|
127
147
|
const job: Job = {
|