tycono 0.1.57 → 0.1.59
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/context-assembler.ts +44 -11
- package/src/api/src/engine/runners/claude-cli.ts +112 -62
- package/src/api/src/routes/save.ts +38 -8
- package/src/api/src/routes/setup.ts +25 -1
- package/src/api/src/services/git-save.ts +115 -0
- package/src/api/src/utils/role-level.ts +12 -25
- package/src/web/dist/assets/index-C-K3xYmU.css +1 -0
- package/src/web/dist/assets/index-Jayn_T3T.js +101 -0
- package/src/web/dist/assets/{preview-app-IdSvrLsi.js → preview-app-Dxb6NBei.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-CShB32ow.css +0 -1
- package/src/web/dist/assets/index-CkQTzzv8.js +0 -101
package/package.json
CHANGED
|
@@ -435,20 +435,37 @@ ${subInfo}
|
|
|
435
435
|
**Use Bash to run the dispatch command:**
|
|
436
436
|
|
|
437
437
|
\`\`\`bash
|
|
438
|
+
# Start a job (returns immediately with job ID)
|
|
438
439
|
python3 "$DISPATCH_CMD" ${exampleSubId} "Task description here"
|
|
440
|
+
|
|
441
|
+
# Check job status/result later
|
|
442
|
+
python3 "$DISPATCH_CMD" --check <jobId>
|
|
443
|
+
|
|
444
|
+
# Start and wait for result (blocks up to 90s)
|
|
445
|
+
python3 "$DISPATCH_CMD" --wait ${exampleSubId} "Task description here"
|
|
439
446
|
\`\`\`
|
|
440
447
|
|
|
441
448
|
**IMPORTANT**: Always use \`python3 "$DISPATCH_CMD"\` — this is the ONLY way to dispatch tasks to subordinates.
|
|
442
449
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
3. Return the subordinate's output if done, or a job ID to check later
|
|
450
|
+
### Recommended Pattern: Parallel Dispatch
|
|
451
|
+
|
|
452
|
+
For multiple tasks, dispatch all at once, then check results:
|
|
447
453
|
|
|
448
|
-
If the subordinate takes longer than 100s, you'll get a job ID. Check the result with:
|
|
449
454
|
\`\`\`bash
|
|
450
|
-
|
|
451
|
-
|
|
455
|
+
# 1. Dispatch all tasks (each returns immediately)
|
|
456
|
+
python3 "$DISPATCH_CMD" ${exampleSubId} "Task A"
|
|
457
|
+
python3 "$DISPATCH_CMD" ${subordinates.length > 1 ? subordinates[1] : exampleSubId} "Task B"
|
|
458
|
+
|
|
459
|
+
# 2. Check results later (use the Job IDs from step 1)
|
|
460
|
+
python3 "$DISPATCH_CMD" --check <jobId-A>
|
|
461
|
+
python3 "$DISPATCH_CMD" --check <jobId-B>
|
|
462
|
+
\`\`\`
|
|
463
|
+
|
|
464
|
+
### Status Values
|
|
465
|
+
- **running** — Subordinate is working
|
|
466
|
+
- **done** — Task completed, result available
|
|
467
|
+
- **error** — Task failed
|
|
468
|
+
- **awaiting_input** — Subordinate has a question for you`;
|
|
452
469
|
|
|
453
470
|
// C-level roles get mandatory delegation rules
|
|
454
471
|
if (isCLevel) {
|
|
@@ -480,19 +497,35 @@ When you receive a directive:
|
|
|
480
497
|
|
|
481
498
|
### The Supervision Loop (CRITICAL)
|
|
482
499
|
|
|
483
|
-
After
|
|
500
|
+
After dispatching tasks, follow this loop:
|
|
484
501
|
|
|
485
502
|
\`\`\`
|
|
486
|
-
DISPATCH →
|
|
487
|
-
|
|
488
|
-
|
|
503
|
+
DISPATCH ALL → CHECK RESULTS → REVIEW → DECIDE
|
|
504
|
+
├── PASS → Knowledge Update → Task Update → Next Dispatch
|
|
505
|
+
└── FAIL → Re-dispatch with feedback
|
|
489
506
|
\`\`\`
|
|
490
507
|
|
|
508
|
+
**Step 1: Dispatch all tasks** (fire-and-forget, collect job IDs)
|
|
509
|
+
\`\`\`bash
|
|
510
|
+
python3 "$DISPATCH_CMD" engineer "Task A" # → job-xxx
|
|
511
|
+
python3 "$DISPATCH_CMD" designer "Task B" # → job-yyy
|
|
512
|
+
\`\`\`
|
|
513
|
+
|
|
514
|
+
**Step 2: Check results** (after waiting, use --check with saved job IDs)
|
|
515
|
+
\`\`\`bash
|
|
516
|
+
python3 "$DISPATCH_CMD" --check <job-xxx>
|
|
517
|
+
python3 "$DISPATCH_CMD" --check <job-yyy>
|
|
518
|
+
\`\`\`
|
|
519
|
+
|
|
520
|
+
**Step 3-4: Review → Knowledge Update → Task Update → Next Dispatch**
|
|
491
521
|
1. **Review**: Does the output meet acceptance criteria?
|
|
492
522
|
2. **Knowledge Update**: Record decisions, findings, analysis in AKB (journals, knowledge/)
|
|
493
523
|
3. **Task Update**: Update task status in tasks.md or project docs
|
|
494
524
|
4. **Next Dispatch**: Identify and dispatch the next task
|
|
495
525
|
|
|
526
|
+
⚠️ Do NOT use curl or other methods to create jobs — always use the dispatch command.
|
|
527
|
+
⚠️ Do NOT use sleep loops to wait — use --check to poll for results.
|
|
528
|
+
|
|
496
529
|
### Dispatch Quality Requirements
|
|
497
530
|
|
|
498
531
|
Every dispatch MUST include:
|
|
@@ -13,9 +13,10 @@ import type { ExecutionRunner, RunnerConfig, RunnerCallbacks, RunnerHandle, Runn
|
|
|
13
13
|
const DISPATCH_SCRIPT = `#!/usr/bin/env python3
|
|
14
14
|
"""dispatch-bridge: CLI runner가 하위 Role에게 작업을 할당하는 브릿지 스크립트.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
dispatch <roleId> "<task>" — Job 시작
|
|
18
|
-
dispatch --check <jobId> —
|
|
16
|
+
3가지 모드:
|
|
17
|
+
dispatch <roleId> "<task>" — Job 시작 (즉시 반환, 대기하지 않음)
|
|
18
|
+
dispatch --check <jobId> — Job 상태 및 결과 조회
|
|
19
|
+
dispatch --wait <roleId> "<task>" — Job 시작 + 완료 대기 (최대 90초)
|
|
19
20
|
|
|
20
21
|
환경변수:
|
|
21
22
|
DISPATCH_API_URL — API 서버 URL (default: http://localhost:3001)
|
|
@@ -44,14 +45,64 @@ def get_result(job_id):
|
|
|
44
45
|
except Exception as e:
|
|
45
46
|
return f'ERROR: Failed to get result: {e}'
|
|
46
47
|
|
|
48
|
+
def get_status(job_id):
|
|
49
|
+
info = json.loads(urllib.request.urlopen(f'{api}/api/jobs/{job_id}', timeout=5).read())
|
|
50
|
+
return info.get('status', 'unknown')
|
|
51
|
+
|
|
52
|
+
def start_job(role_id, task):
|
|
53
|
+
parent_job = os.environ.get('DISPATCH_PARENT_JOB', '')
|
|
54
|
+
source_role = os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo')
|
|
55
|
+
body = json.dumps({
|
|
56
|
+
'type': 'assign',
|
|
57
|
+
'roleId': role_id,
|
|
58
|
+
'task': task,
|
|
59
|
+
'sourceRole': source_role,
|
|
60
|
+
'parentJobId': parent_job if parent_job else None,
|
|
61
|
+
}).encode()
|
|
62
|
+
req = urllib.request.Request(f'{api}/api/jobs', body, {'Content-Type': 'application/json'})
|
|
63
|
+
resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
|
64
|
+
return resp['jobId']
|
|
65
|
+
|
|
66
|
+
def poll_until_done(job_id, role_id, max_wait=90):
|
|
67
|
+
waited = 0
|
|
68
|
+
while waited < max_wait:
|
|
69
|
+
try:
|
|
70
|
+
status = get_status(job_id)
|
|
71
|
+
if status == 'done':
|
|
72
|
+
log('')
|
|
73
|
+
log(f'=== {role_id.upper()} Result (done) ===')
|
|
74
|
+
log(get_result(job_id))
|
|
75
|
+
return
|
|
76
|
+
elif status == 'error':
|
|
77
|
+
log('')
|
|
78
|
+
log(f'=== {role_id.upper()} Result (error) ===')
|
|
79
|
+
log(get_result(job_id))
|
|
80
|
+
return
|
|
81
|
+
elif status == 'awaiting_input':
|
|
82
|
+
log('')
|
|
83
|
+
log(f'{role_id.upper()} is asking a question (awaiting_input).')
|
|
84
|
+
log(f'Check details: python3 "$DISPATCH_CMD" --check {job_id}')
|
|
85
|
+
return
|
|
86
|
+
except Exception:
|
|
87
|
+
pass
|
|
88
|
+
time.sleep(5)
|
|
89
|
+
waited += 5
|
|
90
|
+
if waited % 15 == 0:
|
|
91
|
+
log(f' ... {role_id} still working ({waited}s)')
|
|
92
|
+
log('')
|
|
93
|
+
log(f'{role_id.upper()} is still working after {waited}s.')
|
|
94
|
+
log(f'Check result later: python3 "$DISPATCH_CMD" --check {job_id}')
|
|
95
|
+
|
|
47
96
|
# Mode: --check <jobId>
|
|
48
97
|
if len(sys.argv) >= 3 and sys.argv[1] == '--check':
|
|
49
98
|
job_id = sys.argv[2]
|
|
50
99
|
try:
|
|
51
|
-
|
|
52
|
-
status = info.get('status', 'unknown')
|
|
100
|
+
status = get_status(job_id)
|
|
53
101
|
if status == 'running':
|
|
54
102
|
log(f'Job {job_id} is still running. Try again later.')
|
|
103
|
+
elif status == 'awaiting_input':
|
|
104
|
+
log(f'Job {job_id} is awaiting input (subordinate asked a question).')
|
|
105
|
+
log(get_result(job_id))
|
|
55
106
|
else:
|
|
56
107
|
log(f'=== Job {job_id}: {status} ===')
|
|
57
108
|
log(get_result(job_id))
|
|
@@ -59,33 +110,29 @@ if len(sys.argv) >= 3 and sys.argv[1] == '--check':
|
|
|
59
110
|
log(f'ERROR: {e}')
|
|
60
111
|
sys.exit(0)
|
|
61
112
|
|
|
62
|
-
# Mode:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
# Mode: --wait <roleId> "<task>" (start + wait)
|
|
114
|
+
wait_mode = False
|
|
115
|
+
args = sys.argv[1:]
|
|
116
|
+
if args and args[0] == '--wait':
|
|
117
|
+
wait_mode = True
|
|
118
|
+
args = args[1:]
|
|
119
|
+
|
|
120
|
+
# Usage check
|
|
121
|
+
if len(args) < 2:
|
|
122
|
+
log('Usage: dispatch <roleId> "<task>" — Start job (immediate return)')
|
|
123
|
+
log(' dispatch --wait <roleId> "<task>" — Start job + wait for result')
|
|
124
|
+
log(' dispatch --check <jobId> — Check job status/result')
|
|
66
125
|
subs = os.environ.get('DISPATCH_SUBORDINATES', '')
|
|
67
126
|
if subs:
|
|
68
127
|
log(f'Available subordinates: {subs}')
|
|
69
128
|
sys.exit(1)
|
|
70
129
|
|
|
71
|
-
role_id =
|
|
72
|
-
task = ' '.join(
|
|
73
|
-
parent_job = os.environ.get('DISPATCH_PARENT_JOB', '')
|
|
74
|
-
source_role = os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo')
|
|
130
|
+
role_id = args[0]
|
|
131
|
+
task = ' '.join(args[1:])
|
|
75
132
|
|
|
76
133
|
# Start job
|
|
77
|
-
body = json.dumps({
|
|
78
|
-
'type': 'assign',
|
|
79
|
-
'roleId': role_id,
|
|
80
|
-
'task': task,
|
|
81
|
-
'sourceRole': source_role,
|
|
82
|
-
'parentJobId': parent_job if parent_job else None,
|
|
83
|
-
}).encode()
|
|
84
|
-
|
|
85
134
|
try:
|
|
86
|
-
|
|
87
|
-
resp = json.loads(urllib.request.urlopen(req, timeout=10).read())
|
|
88
|
-
job_id = resp['jobId']
|
|
135
|
+
job_id = start_job(role_id, task)
|
|
89
136
|
except Exception as e:
|
|
90
137
|
log(f'ERROR: Failed to start dispatch job: {e}')
|
|
91
138
|
sys.exit(1)
|
|
@@ -94,26 +141,13 @@ log(f'=== Dispatched to {role_id.upper()} ===')
|
|
|
94
141
|
log(f'Task: {task[:120]}')
|
|
95
142
|
log(f'Job ID: {job_id}')
|
|
96
143
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
while waited < 100:
|
|
101
|
-
try:
|
|
102
|
-
info = json.loads(urllib.request.urlopen(f'{api}/api/jobs/{job_id}', timeout=5).read())
|
|
103
|
-
status = info.get('status', 'unknown')
|
|
104
|
-
if status in ('done', 'error'):
|
|
105
|
-
break
|
|
106
|
-
except Exception:
|
|
107
|
-
pass
|
|
108
|
-
time.sleep(3)
|
|
109
|
-
waited += 3
|
|
110
|
-
|
|
111
|
-
if status in ('done', 'error'):
|
|
112
|
-
log(f'\\n=== {role_id.upper()} Result ({status}) ===')
|
|
113
|
-
log(get_result(job_id))
|
|
144
|
+
if wait_mode:
|
|
145
|
+
log(f'Waiting for completion...')
|
|
146
|
+
poll_until_done(job_id, role_id)
|
|
114
147
|
else:
|
|
115
|
-
log(
|
|
116
|
-
log(f'Check result
|
|
148
|
+
log('')
|
|
149
|
+
log(f'Job started. Check result with:')
|
|
150
|
+
log(f' python3 "$DISPATCH_CMD" --check {job_id}')
|
|
117
151
|
`;
|
|
118
152
|
|
|
119
153
|
/* ─── Consult Bridge Script (Python3) ────── */
|
|
@@ -122,7 +156,7 @@ const CONSULT_SCRIPT = `#!/usr/bin/env python3
|
|
|
122
156
|
"""consult-bridge: CLI runner가 다른 Role에게 질문하는 브릿지 스크립트.
|
|
123
157
|
|
|
124
158
|
사용법:
|
|
125
|
-
consult <roleId> "<question>" — Job 시작 (readOnly) + 결과 대기
|
|
159
|
+
consult <roleId> "<question>" — Job 시작 (readOnly) + 결과 대기 (최대 90초)
|
|
126
160
|
consult --check <jobId> — 완료된 Job 결과 조회
|
|
127
161
|
|
|
128
162
|
환경변수:
|
|
@@ -152,14 +186,20 @@ def get_result(job_id):
|
|
|
152
186
|
except Exception as e:
|
|
153
187
|
return f'ERROR: Failed to get result: {e}'
|
|
154
188
|
|
|
189
|
+
def get_status(job_id):
|
|
190
|
+
info = json.loads(urllib.request.urlopen(f'{api}/api/jobs/{job_id}', timeout=5).read())
|
|
191
|
+
return info.get('status', 'unknown')
|
|
192
|
+
|
|
155
193
|
# Mode: --check <jobId>
|
|
156
194
|
if len(sys.argv) >= 3 and sys.argv[1] == '--check':
|
|
157
195
|
job_id = sys.argv[2]
|
|
158
196
|
try:
|
|
159
|
-
|
|
160
|
-
status = info.get('status', 'unknown')
|
|
197
|
+
status = get_status(job_id)
|
|
161
198
|
if status == 'running':
|
|
162
199
|
log(f'Job {job_id} is still running. Try again later.')
|
|
200
|
+
elif status == 'awaiting_input':
|
|
201
|
+
log(f'Job {job_id} is awaiting input.')
|
|
202
|
+
log(get_result(job_id))
|
|
163
203
|
else:
|
|
164
204
|
log(f'=== Job {job_id}: {status} ===')
|
|
165
205
|
log(get_result(job_id))
|
|
@@ -200,27 +240,37 @@ except Exception as e:
|
|
|
200
240
|
log(f'=== Consulting {role_id.upper()} ===')
|
|
201
241
|
log(f'Question: {question[:120]}')
|
|
202
242
|
log(f'Job ID: {job_id}')
|
|
243
|
+
log(f'Waiting for answer...')
|
|
203
244
|
|
|
204
|
-
# Wait for completion (max ~
|
|
205
|
-
status = 'running'
|
|
245
|
+
# Wait for completion (max ~90s — consult is read-only, usually fast)
|
|
206
246
|
waited = 0
|
|
207
|
-
while waited <
|
|
247
|
+
while waited < 90:
|
|
208
248
|
try:
|
|
209
|
-
|
|
210
|
-
status
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
status = get_status(job_id)
|
|
250
|
+
if status == 'done':
|
|
251
|
+
log('')
|
|
252
|
+
log(f'=== {role_id.upper()} Answer ===')
|
|
253
|
+
log(get_result(job_id))
|
|
254
|
+
sys.exit(0)
|
|
255
|
+
elif status == 'error':
|
|
256
|
+
log('')
|
|
257
|
+
log(f'=== {role_id.upper()} Error ===')
|
|
258
|
+
log(get_result(job_id))
|
|
259
|
+
sys.exit(1)
|
|
260
|
+
elif status == 'awaiting_input':
|
|
261
|
+
log('')
|
|
262
|
+
log(f'{role_id.upper()} needs clarification. Check: python3 "$CONSULT_CMD" --check {job_id}')
|
|
263
|
+
sys.exit(0)
|
|
213
264
|
except Exception:
|
|
214
265
|
pass
|
|
215
|
-
time.sleep(
|
|
216
|
-
waited +=
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
log(f'Check result later: python3 "$CONSULT_CMD" --check {job_id}')
|
|
266
|
+
time.sleep(5)
|
|
267
|
+
waited += 5
|
|
268
|
+
if waited % 15 == 0:
|
|
269
|
+
log(f' ... {role_id} still thinking ({waited}s)')
|
|
270
|
+
|
|
271
|
+
log('')
|
|
272
|
+
log(f'{role_id.upper()} is still thinking after {waited}s.')
|
|
273
|
+
log(f'Check result later: python3 "$CONSULT_CMD" --check {job_id}')
|
|
224
274
|
`;
|
|
225
275
|
|
|
226
276
|
/* ─── Claude CLI Runner ──────────────────────── */
|
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { COMPANY_ROOT } from '../services/file-reader.js';
|
|
3
|
-
import { getGitStatus, gitSave, gitHistory, gitRestore, gitInit } from '../services/git-save.js';
|
|
3
|
+
import { getGitStatus, gitSave, gitHistory, gitRestore, gitInit, gitFetchStatus, gitPull } from '../services/git-save.js';
|
|
4
|
+
import type { RepoType } from '../services/git-save.js';
|
|
4
5
|
|
|
5
6
|
export const saveRouter = Router();
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/** Extract repo type from query param, default 'akb' */
|
|
9
|
+
function getRepo(req: Request): RepoType {
|
|
10
|
+
const repo = req.query.repo;
|
|
11
|
+
return repo === 'code' ? 'code' : 'akb';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// GET /api/save/status?repo=akb|code
|
|
15
|
+
saveRouter.get('/status', (req: Request, res: Response, next: NextFunction) => {
|
|
9
16
|
try {
|
|
10
|
-
res.json(getGitStatus(COMPANY_ROOT));
|
|
17
|
+
res.json(getGitStatus(COMPANY_ROOT, getRepo(req)));
|
|
11
18
|
} catch (err) {
|
|
12
19
|
next(err);
|
|
13
20
|
}
|
|
14
21
|
});
|
|
15
22
|
|
|
16
|
-
// POST /api/save — commit + push
|
|
23
|
+
// POST /api/save?repo=akb|code — commit + push
|
|
17
24
|
saveRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
|
|
18
25
|
try {
|
|
19
26
|
const { message } = req.body ?? {};
|
|
20
|
-
const result = gitSave(COMPANY_ROOT, message);
|
|
27
|
+
const result = gitSave(COMPANY_ROOT, message, getRepo(req));
|
|
21
28
|
res.json({ ok: true, ...result });
|
|
22
29
|
} catch (err) {
|
|
23
30
|
if (err instanceof Error && err.message === 'No changes to save') {
|
|
@@ -28,11 +35,11 @@ saveRouter.post('/', (req: Request, res: Response, next: NextFunction) => {
|
|
|
28
35
|
}
|
|
29
36
|
});
|
|
30
37
|
|
|
31
|
-
// GET /api/save/history
|
|
38
|
+
// GET /api/save/history?repo=akb|code
|
|
32
39
|
saveRouter.get('/history', (req: Request, res: Response, next: NextFunction) => {
|
|
33
40
|
try {
|
|
34
41
|
const limit = Math.min(Number(req.query.limit) || 20, 100);
|
|
35
|
-
res.json(gitHistory(COMPANY_ROOT, limit));
|
|
42
|
+
res.json(gitHistory(COMPANY_ROOT, limit, getRepo(req)));
|
|
36
43
|
} catch (err) {
|
|
37
44
|
next(err);
|
|
38
45
|
}
|
|
@@ -66,3 +73,26 @@ saveRouter.post('/restore', (req: Request, res: Response, next: NextFunction) =>
|
|
|
66
73
|
next(err);
|
|
67
74
|
}
|
|
68
75
|
});
|
|
76
|
+
|
|
77
|
+
// GET /api/save/sync-status?repo=akb|code — fetch + ahead/behind
|
|
78
|
+
saveRouter.get('/sync-status', (req: Request, res: Response, next: NextFunction) => {
|
|
79
|
+
try {
|
|
80
|
+
res.json(gitFetchStatus(COMPANY_ROOT, getRepo(req)));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
next(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// POST /api/save/pull?repo=akb|code — safe pull (ff-only)
|
|
87
|
+
saveRouter.post('/pull', (req: Request, res: Response, next: NextFunction) => {
|
|
88
|
+
try {
|
|
89
|
+
const result = gitPull(COMPANY_ROOT, getRepo(req));
|
|
90
|
+
const statusCode = result.status === 'ok' || result.status === 'up-to-date' ? 200
|
|
91
|
+
: result.status === 'dirty' || result.status === 'diverged' ? 409
|
|
92
|
+
: result.status === 'no-remote' ? 404
|
|
93
|
+
: 500;
|
|
94
|
+
res.status(statusCode).json(result);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
next(err);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
@@ -278,6 +278,16 @@ setupRouter.post('/import-knowledge', (req, res) => {
|
|
|
278
278
|
});
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
/**
|
|
282
|
+
* GET /api/setup/code-root
|
|
283
|
+
* Get the current codeRoot config value.
|
|
284
|
+
*/
|
|
285
|
+
setupRouter.get('/code-root', (_req, res) => {
|
|
286
|
+
const companyRoot = process.env.COMPANY_ROOT || process.cwd();
|
|
287
|
+
const config = readConfig(companyRoot);
|
|
288
|
+
res.json({ codeRoot: config.codeRoot || null });
|
|
289
|
+
});
|
|
290
|
+
|
|
281
291
|
/**
|
|
282
292
|
* POST /api/setup/code-root
|
|
283
293
|
* Set or update the codeRoot config field.
|
|
@@ -297,10 +307,24 @@ setupRouter.post('/code-root', (req, res) => {
|
|
|
297
307
|
return;
|
|
298
308
|
}
|
|
299
309
|
|
|
310
|
+
if (!fs.statSync(resolved).isDirectory()) {
|
|
311
|
+
res.status(400).json({ ok: false, error: 'Path is not a directory' });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if it's a git repository
|
|
316
|
+
let isGitRepo = false;
|
|
317
|
+
try {
|
|
318
|
+
execSync('git rev-parse --git-dir', { cwd: resolved, timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
319
|
+
isGitRepo = true;
|
|
320
|
+
} catch {
|
|
321
|
+
// Not a git repo — still allow, but inform
|
|
322
|
+
}
|
|
323
|
+
|
|
300
324
|
const config = readConfig(companyRoot);
|
|
301
325
|
writeConfig(companyRoot, { ...config, codeRoot: resolved });
|
|
302
326
|
|
|
303
|
-
res.json({ ok: true, codeRoot: resolved });
|
|
327
|
+
res.json({ ok: true, codeRoot: resolved, isGitRepo });
|
|
304
328
|
});
|
|
305
329
|
|
|
306
330
|
/**
|
|
@@ -50,6 +50,22 @@ export interface RestoreResult {
|
|
|
50
50
|
restoredFiles: string[];
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface PullResult {
|
|
54
|
+
status: 'ok' | 'dirty' | 'diverged' | 'up-to-date' | 'no-remote' | 'error';
|
|
55
|
+
message: string;
|
|
56
|
+
commits?: number;
|
|
57
|
+
behind?: number;
|
|
58
|
+
ahead?: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SyncStatus {
|
|
62
|
+
ahead: number;
|
|
63
|
+
behind: number;
|
|
64
|
+
branch: string;
|
|
65
|
+
remote: string;
|
|
66
|
+
hasRemote: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Paths to include in save (relative to root).
|
|
55
71
|
* Only AKB files — never user source code.
|
|
@@ -356,3 +372,102 @@ export function gitRestore(root: string, sha: string, paths?: string[], repo: Re
|
|
|
356
372
|
|
|
357
373
|
return { commitSha: newSha, restoredFiles };
|
|
358
374
|
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Fetch remote and return ahead/behind status
|
|
378
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
379
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
380
|
+
*/
|
|
381
|
+
export function gitFetchStatus(root: string, repo: RepoType = 'akb'): SyncStatus {
|
|
382
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
383
|
+
|
|
384
|
+
if (!isGitRepo(repoRoot)) {
|
|
385
|
+
return { ahead: 0, behind: 0, branch: '', remote: '', hasRemote: false };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
389
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
390
|
+
|
|
391
|
+
if (!hasRemote) {
|
|
392
|
+
return { ahead: 0, behind: 0, branch, remote: '', hasRemote: false };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fetch from remote (timeout 15s for network)
|
|
396
|
+
try {
|
|
397
|
+
execSync('git fetch origin', { cwd: repoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
398
|
+
} catch {
|
|
399
|
+
// Fetch failed (no network, etc.) — return what we know
|
|
400
|
+
return { ahead: 0, behind: 0, branch, remote: 'origin', hasRemote: true };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Count ahead/behind
|
|
404
|
+
const revList = run(`git rev-list --left-right --count HEAD...origin/${branch}`, repoRoot);
|
|
405
|
+
let ahead = 0;
|
|
406
|
+
let behind = 0;
|
|
407
|
+
if (revList) {
|
|
408
|
+
const parts = revList.split(/\s+/);
|
|
409
|
+
ahead = parseInt(parts[0], 10) || 0;
|
|
410
|
+
behind = parseInt(parts[1], 10) || 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return { ahead, behind, branch, remote: 'origin', hasRemote: true };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Safe pull (fast-forward only)
|
|
418
|
+
* @param root - AKB repository root (COMPANY_ROOT)
|
|
419
|
+
* @param repo - Repository type ('akb' or 'code'), default 'akb'
|
|
420
|
+
*/
|
|
421
|
+
export function gitPull(root: string, repo: RepoType = 'akb'): PullResult {
|
|
422
|
+
const repoRoot = resolveRepoRoot(root, repo);
|
|
423
|
+
|
|
424
|
+
if (!isGitRepo(repoRoot)) {
|
|
425
|
+
return { status: 'error', message: 'Not a git repository' };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const branch = run('git rev-parse --abbrev-ref HEAD', repoRoot) || 'unknown';
|
|
429
|
+
const hasRemote = !!run('git remote', repoRoot);
|
|
430
|
+
|
|
431
|
+
if (!hasRemote) {
|
|
432
|
+
return { status: 'no-remote', message: 'No remote configured' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Fetch first
|
|
436
|
+
try {
|
|
437
|
+
execSync('git fetch origin', { cwd: repoRoot, encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
438
|
+
} catch {
|
|
439
|
+
return { status: 'error', message: 'Failed to fetch from remote' };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Check for uncommitted changes
|
|
443
|
+
const porcelain = run('git status --porcelain', repoRoot);
|
|
444
|
+
if (porcelain) {
|
|
445
|
+
return { status: 'dirty', message: 'Uncommitted changes — save or stash before pulling' };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Check ahead/behind
|
|
449
|
+
const revList = run(`git rev-list --left-right --count HEAD...origin/${branch}`, repoRoot);
|
|
450
|
+
let ahead = 0;
|
|
451
|
+
let behind = 0;
|
|
452
|
+
if (revList) {
|
|
453
|
+
const parts = revList.split(/\s+/);
|
|
454
|
+
ahead = parseInt(parts[0], 10) || 0;
|
|
455
|
+
behind = parseInt(parts[1], 10) || 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (behind === 0) {
|
|
459
|
+
return { status: 'up-to-date', message: 'Already up to date', ahead, behind: 0 };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (ahead > 0 && behind > 0) {
|
|
463
|
+
return { status: 'diverged', message: `Branches diverged (${ahead} ahead, ${behind} behind) — manual merge needed`, ahead, behind };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Safe fast-forward pull
|
|
467
|
+
try {
|
|
468
|
+
runOrThrow(`git pull --ff-only origin ${branch}`, repoRoot);
|
|
469
|
+
return { status: 'ok', message: `Pulled ${behind} commit(s)`, commits: behind, ahead: 0, behind: 0 };
|
|
470
|
+
} catch (err) {
|
|
471
|
+
return { status: 'error', message: err instanceof Error ? err.message : 'Pull failed' };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* role-level.ts — Server-side role level calculation
|
|
3
3
|
*
|
|
4
|
-
* Mirrors the frontend level
|
|
5
|
-
*
|
|
4
|
+
* Mirrors the frontend level system.
|
|
5
|
+
* Formula: level = floor(√(tokens ÷ 50,000))
|
|
6
|
+
* Infinite levels, quadratic scaling.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
0, // Lv.1
|
|
10
|
-
10_000, // Lv.2
|
|
11
|
-
50_000, // Lv.3
|
|
12
|
-
150_000, // Lv.4
|
|
13
|
-
400_000, // Lv.5
|
|
14
|
-
1_000_000, // Lv.6
|
|
15
|
-
2_500_000, // Lv.7
|
|
16
|
-
5_000_000, // Lv.8
|
|
17
|
-
10_000_000, // Lv.9
|
|
18
|
-
25_000_000, // Lv.10
|
|
19
|
-
];
|
|
9
|
+
const BASE_XP = 50_000;
|
|
20
10
|
|
|
21
11
|
export function calcLevel(totalTokens: number): number {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return Math.min(level, 10);
|
|
12
|
+
if (totalTokens < BASE_XP) return 1;
|
|
13
|
+
return Math.max(1, Math.floor(Math.sqrt(totalTokens / BASE_XP)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function tokensForLevel(level: number): number {
|
|
17
|
+
return BASE_XP * level * level;
|
|
30
18
|
}
|
|
31
19
|
|
|
32
20
|
export function calcProgress(totalTokens: number): number {
|
|
33
21
|
const level = calcLevel(totalTokens);
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const next = THRESHOLDS[level];
|
|
22
|
+
const current = tokensForLevel(level);
|
|
23
|
+
const next = tokensForLevel(level + 1);
|
|
37
24
|
return Math.min(1, (totalTokens - current) / (next - current));
|
|
38
25
|
}
|
|
39
26
|
|