genetics-mcp 2.8.0 → 2.9.0
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/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/manifest.json +2 -2
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/genetics.mjs +219 -24
- package/package.json +1 -1
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
2
|
+
"aarch64-apple-darwin": "3f5fdf3c6029d94eafdb4ecf34f502676b3262f15de05eb54ecf6483d2d0f829",
|
|
3
3
|
"aarch64-unknown-linux-musl": "5588db11a4aad41a65609d4cf14f389d106fb8b74023aa745ae4765738243006",
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
4
|
+
"x86_64-apple-darwin": "8c440a19d2aee09dc8e2c3fe0a1b513563a7ab023d97722b99e5bae8eeb2cc62",
|
|
5
5
|
"x86_64-unknown-linux-musl": "f01bf90ab7014764487773b9ac91ad8532ac6a727eedd6ff50151c3e6eab127e"
|
|
6
6
|
}
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -635,12 +635,65 @@ const REPORT_DATA = {
|
|
|
635
635
|
* 믿는데 실제로는 무시된다 — 거짓말하는 인자다. 고를 것이 생기면 그때 연다.
|
|
636
636
|
*/
|
|
637
637
|
title_tag: { type: 'string', description: '브라우저 탭 제목' },
|
|
638
|
-
title: { type: 'string', description: '표지
|
|
638
|
+
title: { type: 'string', description: '표지 대제목 **첫 행** (굵게)' },
|
|
639
|
+
title_sub: {
|
|
640
|
+
type: 'string',
|
|
641
|
+
description: '표지 대제목 **둘째 행** — 회차·부제. "1차 보고" 처럼 **짧게**. '
|
|
642
|
+
+ '여기는 42px 표제 자리라 문장을 넣으면 조판이 무너진다. 한 문단 요약은 subtitle 로.',
|
|
643
|
+
},
|
|
639
644
|
kicker: { type: 'string', description: '표지 윗줄 (예: "회사명 · 프로젝트")' },
|
|
640
|
-
subtitle: { type: 'string', description: '표지 한 문단 요약' },
|
|
645
|
+
subtitle: { type: 'string', description: '표지 한 문단 요약 — 대제목 **아래**에 앉는다' },
|
|
641
646
|
description: { type: 'string' },
|
|
642
|
-
|
|
643
|
-
|
|
647
|
+
doc_no: { type: 'string', description: '문서번호. 표지 머리 왼쪽 박스 (예: "LIM-2026-0824-01")' },
|
|
648
|
+
client: {
|
|
649
|
+
type: 'string',
|
|
650
|
+
description: '고객사명. 표지 머리 **오른쪽**은 고객사 마크의 자리다 — 로고 파일을 '
|
|
651
|
+
+ '받았으면 assets 에 role:"logo" 로 넣고, 못 받았으면 이 이름을 활자로 세운다. '
|
|
652
|
+
+ '비워 두지 마라: 빈 자리는 「받을 것을 못 받은 문서」로 읽힌다.',
|
|
653
|
+
},
|
|
654
|
+
issuer: {
|
|
655
|
+
type: 'array',
|
|
656
|
+
description: '표지 맨 아래 발행처 스트립 ["발행처", "대상", "2026. 8."] — 마지막 칸이 날짜다.',
|
|
657
|
+
},
|
|
658
|
+
meta: { type: 'array', description: '표지 메타 [[라벨, 값], ...] · 4칸 권장' },
|
|
659
|
+
stats: { type: 'array', description: '요약면 수치 칸 [{value, unit, label, accent?, href?}] · accent:true 는 **독자 몫**인 첫 칸' },
|
|
660
|
+
history: {
|
|
661
|
+
type: 'array',
|
|
662
|
+
description: '발행 이력 [{no, date, note}] — 회차 문서임을 문서가 스스로 밝히는 자리.',
|
|
663
|
+
},
|
|
664
|
+
guide: {
|
|
665
|
+
type: 'array',
|
|
666
|
+
description: '이 문서 읽는 법 [{q, a: [문장, ...]}] — **독자가 실제로 던질 질문**만. '
|
|
667
|
+
+ '「시간이 없다면 어디를 봐야 하는가?」 「어디까지 확인한 것인가?」 처럼. '
|
|
668
|
+
+ '채우려고 넣으면 안 넣느니만 못하다.',
|
|
669
|
+
},
|
|
670
|
+
parts: {
|
|
671
|
+
type: 'array',
|
|
672
|
+
description: 'PART 묶음 [{no, title, lede?, tone?, questions?}] — 절만 늘어놓으면 '
|
|
673
|
+
+ '「먼저 읽을 것 / 경과 / 남은 것」이라는 독자의 읽기 순서가 사라진다. '
|
|
674
|
+
+ 'tone 은 you(독자 몫)·neutral(기록)·wait(대기)·hold(유보) 중 하나. '
|
|
675
|
+
+ 'questions 는 「이 PART 가 답하는 질문」.',
|
|
676
|
+
},
|
|
677
|
+
notice: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
description: '요약면 현황 줄 {state, text} — state 는 critical|info|success. '
|
|
680
|
+
+ '**상태에 따라 바뀌는 유일한 줄**이다. 떠 있는 알림 배너를 따로 두지 마라.',
|
|
681
|
+
},
|
|
682
|
+
bar: { type: 'array', description: '요약면 상태 막대 [{value, label, tone}] · tone 은 done|wait' },
|
|
683
|
+
bar_note: { type: 'string', description: '막대가 뜻하는 바의 한정 조건 (※ 각주)' },
|
|
684
|
+
bar_alt: { type: 'string', description: '막대의 대체 텍스트 (보조기기용)' },
|
|
685
|
+
appendix: {
|
|
686
|
+
type: 'array',
|
|
687
|
+
description: '부록 [{no, title, rows: [[라벨, 값], ...], note?}] — 본문에서 **반복 참조되는** '
|
|
688
|
+
+ '기준·환경을 뒤로 뺀다(확인 환경, 판정 기준 등).',
|
|
689
|
+
},
|
|
690
|
+
colophon: {
|
|
691
|
+
type: 'object',
|
|
692
|
+
description: '판권지 {title?, sub?, rows?, mark?, badge?, note?, tail?} — 표지와 같은 문법으로 '
|
|
693
|
+
+ '문서를 닫는다. badge 는 「내부 공유용」 같은 표시.',
|
|
694
|
+
},
|
|
695
|
+
remain: { type: 'string', description: '머리띠 우측 판독부의 수치(요청 잔여 건수). 생략하면 open_items 개수.' },
|
|
696
|
+
running_footer: { type: 'string', description: '인쇄 러닝 푸터의 뒷말. 생략하면 client.' },
|
|
644
697
|
grades: { type: 'object', description: '확인 등급 정의 {이름: [클래스, 설명]}' },
|
|
645
698
|
groups: { type: 'array', description: '건 묶음 [{key, no}]' },
|
|
646
699
|
sections: {
|
|
@@ -656,7 +709,21 @@ const REPORT_DATA = {
|
|
|
656
709
|
+ '· `logic`: {kicker, title_lines[], lede, models:[{label, flow[], body}, …]} — 대조 도해. '
|
|
657
710
|
+ '`flow` 는 「현재 상태 → 인접 등급 → 도착점」 같은 단계이고 **마지막 칸이 강조**된다. '
|
|
658
711
|
+ '첫 모델이 「현재」, 그다음이 「개선」으로 그려진다(자리로 정한다).\n'
|
|
659
|
-
+ '둘 다 **데이터가 있을 때만** 그려진다 — 없는 문구를 지어내지 말
|
|
712
|
+
+ '둘 다 **데이터가 있을 때만** 그려진다 — 없는 문구를 지어내지 말 것.\n'
|
|
713
|
+
+ '\n'
|
|
714
|
+
+ '관보 문법이 절마다 요구하는 칸:\n'
|
|
715
|
+
+ '· `part`: 이 절이 속한 PART 의 no. 안 주면 첫 묶음으로 떨어진다.\n'
|
|
716
|
+
+ '· `marg`: {why, key, advice} — **좌측 여백 주석 단. 이것이 본체다.** '
|
|
717
|
+
+ '없으면 두 단 본문이 한 단으로 서고, 문서는 「문서처럼 꾸민 웹페이지」가 된다. '
|
|
718
|
+
+ 'why 는 **독자가 실제로 던질 질문**(「무엇을 조치해야 하는가?」), '
|
|
719
|
+
+ 'key 는 이 절에서 하나만 가져간다면 그것, advice 는 어디를 참조하라는 것. '
|
|
720
|
+
+ '채우려고 넣으면 안 넣느니만 못하다.\n'
|
|
721
|
+
+ '· `lede`: 절 간지의 도입문 — 다루는 범위와 **다루지 않는 범위**.\n'
|
|
722
|
+
+ '· `sub`: 차례 행에 붙는 한 줄 설명. `short`: 측면 색인 레일용 짧은 이름.\n'
|
|
723
|
+
+ '· `tone`: 장 색을 뜻으로 고른다 — you(독자 몫)·deep(요지)·neutral(허브)·'
|
|
724
|
+
+ 'done(끝난 것)·wait(대기)·hold(유보). 생략하면 kind 가 정한다.\n'
|
|
725
|
+
+ '· kind:status 전용 — `board_title`(대조표 제목) · `board_hint`(표 아래 안내).\n'
|
|
726
|
+
+ '· kind:note 전용 — `paragraphs`(강조 박스의 항목) · `limit_label`(그 박스 제목).',
|
|
660
727
|
},
|
|
661
728
|
chapters: {
|
|
662
729
|
type: 'array',
|
|
@@ -722,8 +789,11 @@ const REPORT_DATA = {
|
|
|
722
789
|
properties: {
|
|
723
790
|
id: { type: 'string', description: '영소문자·숫자·하이픈 1~40자. 문서 안 참조 이름' },
|
|
724
791
|
role: {
|
|
725
|
-
type: 'string', enum: ['hero', 'chapter', 'figure', 'eof'],
|
|
726
|
-
description: '`
|
|
792
|
+
type: 'string', enum: ['hero', 'chapter', 'figure', 'eof', 'logo'],
|
|
793
|
+
description: '`logo` 는 **표지 우상단 고객사 마크**다 — provider 는 user-provided, '
|
|
794
|
+
+ 'origin 에 제공자를 적는다. 못 받았으면 넣지 말고 data.client 에 이름만 적어라 '
|
|
795
|
+
+ '(렌더러가 활자로 세운다).\n'
|
|
796
|
+
+ '`eof` 는 **문서 끝 꼬리 카드**다. 안 줘도 된다 — 서버가 기본 카드를 '
|
|
727
797
|
+ '끼운다(계약 9절). 이 문서만의 카드가 있을 때만 준다. 그때는 무손실(PNG 또는 '
|
|
728
798
|
+ '무손실 WebP)이어야 하고 줄이지 않고 원본 크기로 실린다.',
|
|
729
799
|
},
|
|
@@ -1537,7 +1607,36 @@ function beatWell() {
|
|
|
1537
1607
|
/* 자동 올리기 — **서버 사본이 낡았을 때만.** 사용자 결정(2026-08-20)이라 기본이 켜짐이고,
|
|
1538
1608
|
* 끄는 길을 함께 둔다(`GENETICS_NO_AUTO_PUSH=1`). 이 값은 「내부 대화가 서버에 쌓인다」를
|
|
1539
1609
|
* 뜻하므로, 끄는 길이 없으면 결정이 아니라 함정이 된다. */
|
|
1540
|
-
|
|
1610
|
+
/**
|
|
1611
|
+
* 이 텍스트와 환경변수로 **자동 올리기가 켜졌는지**. 순수 함수 — 시험이 걷는다.
|
|
1612
|
+
*
|
|
1613
|
+
* ⚠ 위 `cfg` 와 **같은 우선순위**: 환경변수가 파일을 이긴다. 반대로 짰다가 실제 사고를
|
|
1614
|
+
* 냈다(2026-08-12) — 오버라이드가 안 먹는 설정은 안전한 기본값이 아니라 함정이다.
|
|
1615
|
+
*/
|
|
1616
|
+
function autoPushFromText(text, env) {
|
|
1617
|
+
const K = 'GENETICS_NO_AUTO_PUSH';
|
|
1618
|
+
if (env && Object.prototype.hasOwnProperty.call(env, K)) return !env[K];
|
|
1619
|
+
let v = '';
|
|
1620
|
+
for (const line of String(text || '').split('\n')) {
|
|
1621
|
+
const m = line.match(/^\s*([A-Z_]+)\s*=\s*(.*)\s*$/);
|
|
1622
|
+
if (m && m[1] === K) v = m[2];
|
|
1623
|
+
}
|
|
1624
|
+
return !v;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
/**
|
|
1628
|
+
* **스위치를 매번 다시 읽는다.** 예전에는 시작할 때 한 번만 읽어서, 켠 사람이 같은
|
|
1629
|
+
* 「꺼져 있습니다」를 다시 보고 「안 켜졌다」고 읽었다 — 2026-08-24 내가 그 함정에 빠졌다.
|
|
1630
|
+
*
|
|
1631
|
+
* 안내문으로 덮는 것보다 **켜면 켜지는 것**이 맞다. 파일은 200바이트 남짓이고 심박은
|
|
1632
|
+
* 20초에 한 번이라 비용이 없다. 자격은 그대로 시작할 때 값을 쓴다 — 여기서 다시 읽는 것은
|
|
1633
|
+
* **사람이 쥐는 스위치 하나**뿐이다.
|
|
1634
|
+
*/
|
|
1635
|
+
function autoPushOn() {
|
|
1636
|
+
let text = '';
|
|
1637
|
+
try { text = fs.readFileSync(ENV_FILE, 'utf8'); } catch { /* 없으면 env 로만 */ }
|
|
1638
|
+
return autoPushFromText(text, process.env);
|
|
1639
|
+
}
|
|
1541
1640
|
|
|
1542
1641
|
/*
|
|
1543
1642
|
* ⚠⚠ **꺼져 있으면 말한다.** 전에는 `if (!AUTO_PUSH) return;` 한 줄로 조용히 빠졌다.
|
|
@@ -1550,26 +1649,21 @@ const AUTO_PUSH = !cfg.GENETICS_NO_AUTO_PUSH;
|
|
|
1550
1649
|
* 이 저장소는 세포를 필수로 만들며 규칙을 세웠다: **「없으면 막지 말고 알려주면서 유도한다」.**
|
|
1551
1650
|
* 같은 규칙이 여기에도 걸린다. 끄는 것은 사람의 선택이지만, **끈 줄 모르는 것은 선택이 아니다.**
|
|
1552
1651
|
*/
|
|
1553
|
-
let autoPushSaid =
|
|
1652
|
+
let autoPushSaid = null; // 마지막으로 말한 상태 — 스위치가 바뀌면 다시 말한다
|
|
1554
1653
|
function autoPushSilence() {
|
|
1555
|
-
if (autoPushSaid) return;
|
|
1556
|
-
autoPushSaid =
|
|
1654
|
+
if (autoPushSaid === false) return;
|
|
1655
|
+
autoPushSaid = false;
|
|
1557
1656
|
// stdout 은 프로토콜이다 — 사람이 보는 자리(stderr)로만 말한다.
|
|
1558
1657
|
process.stderr.write(
|
|
1559
1658
|
'genetics: 대화 본문 자동 올리기가 꺼져 있다(GENETICS_NO_AUTO_PUSH). '
|
|
1560
1659
|
+ '이 기계의 세션은 서버에 안 쌓이고, 그래서 두뇌가 그 대화로 팀 기억을 못 굽는다.\n'
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
* 옛 값을 든 채다 — 그래서 켠 사람이 같은 문장을 다시 보고 「안 켜졌다」고 읽는다.
|
|
1564
|
-
* 실측 2026-08-24: 내가 그 함정에 빠졌다. 「끈 줄 모르는 것은 선택이 아니다」와 같은 이유로
|
|
1565
|
-
* **켠 줄 모르는 것도 선택이 아니다.**
|
|
1566
|
-
*/
|
|
1567
|
-
+ ' (설정을 방금 바꿨다면 이 창을 다시 열어야 반영된다 — 시작할 때 한 번만 읽는다.)\n');
|
|
1660
|
+
// 스위치는 심박마다 다시 읽는다 — 켜면 다음 심박(20초)에 스스로 켜진다.
|
|
1661
|
+
+ ' (지금 켜면 다음 심박에 스스로 반영된다 — 창을 다시 열 필요 없다.)\n');
|
|
1568
1662
|
}
|
|
1569
1663
|
const autoPushAt = new Map();
|
|
1570
1664
|
|
|
1571
1665
|
async function autoPushIfAhead(reply, recs) {
|
|
1572
|
-
if (!
|
|
1666
|
+
if (!autoPushOn()) { autoPushSilence(); return; }
|
|
1573
1667
|
if (!reply) return;
|
|
1574
1668
|
const now = Date.now();
|
|
1575
1669
|
for (const v of reply) {
|
|
@@ -1606,7 +1700,7 @@ async function beatTick() {
|
|
|
1606
1700
|
beatRunning = true;
|
|
1607
1701
|
try {
|
|
1608
1702
|
// 새 세션을 같은 폴더의 앞 세션과 같은 소속처로 등록한다(색인만 — 올리기는 아래가 한다).
|
|
1609
|
-
if (
|
|
1703
|
+
if (autoPushOn()) { try { indexAdoptCurrent(); } catch { /* 등록 실패가 심박을 멈추면 안 된다 */ } }
|
|
1610
1704
|
const recs = beatTargets();
|
|
1611
1705
|
if (!recs.length) return;
|
|
1612
1706
|
// **안 바뀌었으면 안 보낸다.** 이 설계가 실제로 버는 것은 바이트가 아니라 이것이다.
|
|
@@ -1616,6 +1710,8 @@ async function beatTick() {
|
|
|
1616
1710
|
beatLastSent = Date.now();
|
|
1617
1711
|
const reply = await beatOnce(recs);
|
|
1618
1712
|
await autoPushIfAhead(reply, recs);
|
|
1713
|
+
// 두뇌가 짚은 모순을 긷는다 — 실패해도 심박을 안 멈춘다(안 오는 것이 고장은 아니다).
|
|
1714
|
+
try { await brainPull(); } catch { /* 그물·자격 문제는 다음 바퀴에 다시 */ }
|
|
1619
1715
|
} finally {
|
|
1620
1716
|
beatRunning = false;
|
|
1621
1717
|
}
|
|
@@ -2244,7 +2340,99 @@ function cellNotice(why) {
|
|
|
2244
2340
|
].join('\n');
|
|
2245
2341
|
}
|
|
2246
2342
|
|
|
2343
|
+
/* ── ★★ 백신 받기 — 두뇌가 짚은 **모순**을 이 세션 눈앞에 놓는다 ──────────────
|
|
2344
|
+
*
|
|
2345
|
+
* 사용자의 진단(2026-08-24): 여러 세포가 한 군체를 다룰 때 **정상적으로 일하는 사람이
|
|
2346
|
+
* 두뇌에겐 바이러스가 된다.** 그러면 두뇌가 그 세션에게 **되돌려 줄 줄 알아야** 한다.
|
|
2347
|
+
*
|
|
2348
|
+
* ── 왜 세포(gcell)가 아니라 여기서 긷나 ────────────────────────────────────
|
|
2349
|
+
* 세포도 같은 장부를 긷지만 `kind != "write"` 를 **버린다**(`cell/src/server.rs:254`).
|
|
2350
|
+
* 통과시켜도 `root.join(ref)` 로 ref 를 경로 취급해 **없는 파일**을 「최근에 바뀐 것」으로
|
|
2351
|
+
* 만든다 — 그 파일의 주석이 바로 그 해악을 경고한다. 세포를 다시 굽는 것은 별건이라,
|
|
2352
|
+
* 검토는 이 자리에서 **직접** 긷는다(사람 자격으로 열리는 창구다).
|
|
2353
|
+
*
|
|
2354
|
+
* ── 처음 한 바퀴에 옛날 것을 읊지 않는다 ────────────────────────────────────
|
|
2355
|
+
* 커서가 없으면 지난 것을 전부 「지금 일」로 읽는다. 그래서 첫 바퀴는 **커서만 맞추고**
|
|
2356
|
+
* 최근 10분 것만 말한다. 커서는 색인에 적어 두므로 이 프로세스가 죽었다 살아나도 안 잃는다.
|
|
2357
|
+
*/
|
|
2358
|
+
const BRAIN_POLL_MS = 120_000; // 굽기는 분 단위다 — 20초 심박마다 두드릴 이유가 없다
|
|
2359
|
+
const BRAIN_FRESH_MS = 600_000; // 첫 바퀴에 「방금 것」으로 볼 나이
|
|
2360
|
+
const BRAIN_PAGE = 200; // 서버 상한(WORLD_PAGE_MAX)과 같다
|
|
2361
|
+
let brainAt = 0;
|
|
2362
|
+
let brainCursor = null; // null = 아직 색인에서 안 읽었다
|
|
2363
|
+
const brainQueue = []; // 아직 안 말한 모순
|
|
2364
|
+
|
|
2365
|
+
async function brainPull() {
|
|
2366
|
+
const mine = process.env.CLAUDE_CODE_SESSION_ID || '';
|
|
2367
|
+
if (!mine) return;
|
|
2368
|
+
const e = indexRead()[mine];
|
|
2369
|
+
if (!e?.tenant || !e?.tenant_id) return; // 아직 어느 소속처인지 모른다
|
|
2370
|
+
const cred = credential();
|
|
2371
|
+
// **키로는 세계 장부를 못 연다** — 서버가 사람 토큰만 받는다(`api.php` 의 world 라우트).
|
|
2372
|
+
if (!cred || cred.kind !== 'user') return;
|
|
2373
|
+
if (Date.now() - brainAt < BRAIN_POLL_MS) return;
|
|
2374
|
+
brainAt = Date.now();
|
|
2375
|
+
|
|
2376
|
+
const first = brainCursor === null;
|
|
2377
|
+
if (first) brainCursor = Number(e.brain_cursor || 0);
|
|
2378
|
+
const cwd = relHome(process.cwd());
|
|
2379
|
+
const fresh = [];
|
|
2380
|
+
for (let page = 0; page < 10; page++) {
|
|
2381
|
+
const r = await api('GET', '/api/world?tenant=' + encodeURIComponent(e.tenant)
|
|
2382
|
+
+ '&since=' + brainCursor + '&cwd=' + encodeURIComponent(cwd) + '&limit=' + BRAIN_PAGE);
|
|
2383
|
+
const evs = Array.isArray(r?.events) ? r.events : [];
|
|
2384
|
+
for (const ev of evs) if (ev?.kind === 'review' && ev?.ref) fresh.push(ev);
|
|
2385
|
+
if (typeof r?.cursor === 'number' && r.cursor > brainCursor) brainCursor = r.cursor;
|
|
2386
|
+
if (!r?.truncated) break;
|
|
2387
|
+
}
|
|
2388
|
+
indexNote(mine, { tenant: e.tenant, tenant_id: e.tenant_id, brain_cursor: brainCursor });
|
|
2389
|
+
|
|
2390
|
+
const now = Date.now();
|
|
2391
|
+
for (const ev of fresh) {
|
|
2392
|
+
if (!brainFresh(ev, first, now)) continue;
|
|
2393
|
+
brainQueue.push(String(ev.ref));
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* 이 사건을 **지금 일로 말해도 되나.**
|
|
2399
|
+
*
|
|
2400
|
+
* 커서가 있는 바퀴(=이 프로세스가 이미 따라잡은 뒤)라면 내려온 것은 정의상 새것이라 전부 참.
|
|
2401
|
+
* **첫 바퀴만** 나이를 본다 — 커서가 0이면 지난 것이 통째로 내려오는데, 그것을 읊으면
|
|
2402
|
+
* 이미 끝난 이야기를 **지금 하는 일로** 읽는다. 그게 이 물건이 막으려던 바로 그 병이다.
|
|
2403
|
+
*
|
|
2404
|
+
* ⚠ 시각을 못 읽으면 **안 말한다.** 모르는 것을 「방금」으로 치는 쪽이 더 위험하다.
|
|
2405
|
+
*/
|
|
2406
|
+
function brainFresh(ev, first, now) {
|
|
2407
|
+
if (!first) return true;
|
|
2408
|
+
const at = Date.parse(String(ev?.at || '')) || 0;
|
|
2409
|
+
return at > 0 && now - at <= BRAIN_FRESH_MS;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* 큐에 쌓인 모순을 **한 번씩만** 말한다.
|
|
2414
|
+
*
|
|
2415
|
+
* ⚠ **지시를 안 붙인다.** 실측 2026-08-22: 「…라고 말하라」를 실었더니 모델이 *「가짜 시스템」*
|
|
2416
|
+
* 이라며 거부했다 — 옳은 거부다. 서버도 `world_event_clean()` 으로 지시형을 막는다.
|
|
2417
|
+
* 여기서는 **어디를 보면 되는지**만 덧붙인다(사실).
|
|
2418
|
+
*/
|
|
2419
|
+
function brainSays() {
|
|
2420
|
+
if (!brainQueue.length) return '';
|
|
2421
|
+
const n = brainQueue.length;
|
|
2422
|
+
const lines = brainQueue.splice(0, n).map((t) => ` · ${t}`);
|
|
2423
|
+
return '두뇌가 이 폴더의 지식에서 모순을 짚었다:\n' + lines.join('\n')
|
|
2424
|
+
+ '\n전문은 포털 지식 화면의 검토 목록에 있다. search_cases 로 그 주제를 찾으면'
|
|
2425
|
+
+ ' 「⚠ 상태:」가 붙은 장이 어느 것인지 보인다.';
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2247
2428
|
function worldAppend(result) {
|
|
2429
|
+
/*
|
|
2430
|
+
* ★ 백신이 **먼저**다. 세계 델타는 「누가 무엇을 썼다」는 배경이고, 모순은 지금 하는 일을
|
|
2431
|
+
* 바꿀 수 있는 사실이다. 그리고 델타가 비어도(조용한 구간) 백신은 나가야 한다 —
|
|
2432
|
+
* 아래 `if (!t) return;` 뒤에 두면 **조용한 날엔 영영 안 간다.**
|
|
2433
|
+
*/
|
|
2434
|
+
const v = brainSays();
|
|
2435
|
+
if (v) result.content.push({ type: 'text', text: v });
|
|
2248
2436
|
const t = worldDelta();
|
|
2249
2437
|
if (!t) return;
|
|
2250
2438
|
/*
|
|
@@ -2365,11 +2553,11 @@ async function callTool(name, args = {}) {
|
|
|
2365
2553
|
* 지식에 없지」를 사람이 짐작해야 한다(2026-08-23 실측: 하루가 통째로 비었다).
|
|
2366
2554
|
* **사실만 적는다** — 켜라고 하지 않는다. 끄는 것은 사람의 선택이다.
|
|
2367
2555
|
*/
|
|
2368
|
-
const pushOff =
|
|
2556
|
+
const pushOff = autoPushOn() ? '' :
|
|
2369
2557
|
'\n대화 본문 자동 올리기는 꺼져 있습니다(GENETICS_NO_AUTO_PUSH) — '
|
|
2370
2558
|
+ '이 기계의 세션은 서버에 안 쌓이고, 두뇌가 그 대화로 팀 기억을 굽지 않습니다.'
|
|
2371
|
-
//
|
|
2372
|
-
+ '\n (
|
|
2559
|
+
// 스위치는 심박마다 다시 읽는다 — 창을 다시 열 필요가 없다.
|
|
2560
|
+
+ '\n (지금 켜시면 다음 심박에 스스로 반영됩니다 — 창을 다시 열 필요 없습니다.)';
|
|
2373
2561
|
return `${me.user.name}(${me.user.email}) · ${me.user.role} 으로 붙어 있습니다.\n`
|
|
2374
2562
|
+ `기계: ${me.device_label || '(이름 없음)'} · 만료 ${c.expires_at}${alsoKey}${pushOff}`;
|
|
2375
2563
|
}
|
|
@@ -2588,7 +2776,14 @@ async function callTool(name, args = {}) {
|
|
|
2588
2776
|
+ '이번에 고치면 remember_case 로 남길 것.';
|
|
2589
2777
|
}
|
|
2590
2778
|
return `«${args.q}» — ${r.hits.length}건(${r.scanned}장 중${r.expanded ? ', 의미 확장으로 다시 찾음' : ''})\n`
|
|
2591
|
-
|
|
2779
|
+
/*
|
|
2780
|
+
* ★ **뒤집힌 결정을 살아 있는 것으로 읽지 않게 한다.** 점수로는 구별이 안 되고,
|
|
2781
|
+
* 찾는 쪽은 위에 뜬 것을 지금 참인 것으로 읽는다 — 그렇게 낡은 결정이 다음
|
|
2782
|
+
* 세포에게 전달된다. 「유효」는 **서버가 걸러서** 안 온다 — 그 규칙을 여기 두면
|
|
2783
|
+
* 화면과 갈린다(`wiki_page_status()` 머리말).
|
|
2784
|
+
*/
|
|
2785
|
+
+ r.hits.map((h) => `· ${h.path} [${h.score}점]${h.status ? `\n ⚠ 상태: ${h.status}` : ''}`
|
|
2786
|
+
+ `\n ${h.excerpt}`).join('\n')
|
|
2592
2787
|
+ '\n전문은 get 이 아니라 포털 지식 화면에서 본다(경로 그대로).';
|
|
2593
2788
|
}
|
|
2594
2789
|
case 'list_sessions': {
|