neurain 0.1.0-alpha.8 → 0.1.0-alpha.9
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/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/docs/development-status.en.md +3 -3
- package/docs/development-status.kr.md +3 -3
- package/package.json +1 -1
- package/src/core/capture_durable.mjs +3 -22
- package/src/core/compile_desk.mjs +3 -27
- package/src/core/render.mjs +179 -0
- package/src/core/status.mjs +31 -28
- package/src/core/sync.mjs +3 -3
- package/src/mcp/server.mjs +11 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
- No unreleased changes recorded.
|
|
6
6
|
|
|
7
|
+
## 0.1.0-alpha.9
|
|
8
|
+
|
|
9
|
+
- Output consistency (single render source): added `src/core/render.mjs` as the one place that renders user-facing command output, so every host shows the same block. `status`, `compile`, `sync`, and `capture` now build their output via `renderX(payload)`, expose it as `payload.rendered`, and return it as `{ text }` (the existing `digest.mjs` pattern). The MCP server's user-facing view tools (`neurain_session_status`, `neurain_compile`, `neurain_digest_preview`) now return that rendered markdown block instead of `JSON.stringify(payload)`, so an MCP host and the CLI emit byte-identical bytes; diagnostic/eval MCP tools still return JSON. Output is clean GitHub-flavored markdown tables, ASCII only, with status as words (ok/warn/missing/blocked) and no status glyphs or dashes. The `--json` payload shape is unchanged (machine consumers unaffected). Added `test/render.test.mjs` (per-renderer ascii/table/determinism, CLI-text == payload.rendered, MCP returns markdown not JSON). npm test 167/167, readiness 100.
|
|
10
|
+
|
|
11
|
+
|
|
7
12
|
## 0.1.0-alpha.8
|
|
8
13
|
|
|
9
14
|
- Privacy (exact recall freshness): the exact-token branch reads from the SQLite FTS index, which is a cache rebuilt only on demand, so a markdown file that was public when indexed but has since turned `sensitivity: private`, been deleted, or gained a secret could linger in `recall search` and `hybrid-search` exact results until the next rebuild. `searchRecall` now re-gates the returned markdown paths against the CURRENT files (same private/secret/exists gate the markdown branches already apply) and drops any that are no longer safe. Only the top-K returned paths are re-read, so the exact branch stays fast (~4ms warm); a fresh index drops nothing, so results are unchanged (golden 9/9 identical). Event and receipt rows keep their own collection-time gating and pass through. Added `test/perf_recall_equivalence.test.mjs` coverage that a public-then-private and a deleted file do not surface from a stale index.
|
package/README.md
CHANGED
|
@@ -204,7 +204,7 @@ It exposes read/capture/scan/preview tools only. It does not silently compile, p
|
|
|
204
204
|
|
|
205
205
|
## Status
|
|
206
206
|
|
|
207
|
-
This is `0.1.0-alpha.
|
|
207
|
+
This is `0.1.0-alpha.9`. It is not a public SaaS GA release. The alpha exists to prove installability, local-first onboarding, Codex, Claude, Gemini, and Runtime connectivity, plus safety receipts.
|
|
208
208
|
|
|
209
209
|
Alpha publish command:
|
|
210
210
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Development Status
|
|
2
2
|
|
|
3
3
|
Version: v0.1
|
|
4
|
-
Last updated: 2026-06-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
4
|
+
Last updated: 2026-06-21 KST
|
|
5
|
+
Package: `neurain@0.1.0-alpha.9`
|
|
6
|
+
Latest documented commit: `291ba2e feat(render): single engine render source; MCP relays rendered markdown`
|
|
7
7
|
|
|
8
8
|
This document is the canonical product development snapshot for the public package. It tracks what is shipped, what has evidence, and what must not be claimed yet.
|
|
9
9
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# 개발 진행 상태
|
|
2
2
|
|
|
3
3
|
Version: v0.1
|
|
4
|
-
Last updated: 2026-06-
|
|
5
|
-
Package: `neurain@0.1.0-alpha.
|
|
6
|
-
Latest documented commit: `
|
|
4
|
+
Last updated: 2026-06-21 KST
|
|
5
|
+
Package: `neurain@0.1.0-alpha.9`
|
|
6
|
+
Latest documented commit: `291ba2e feat(render): single engine render source; MCP relays rendered markdown`
|
|
7
7
|
|
|
8
8
|
이 문서는 public package 기준의 canonical 개발 상태 스냅샷입니다. 무엇이 shipped인지, 어떤 증거가 있는지, 아직 주장하면 안 되는 것이 무엇인지 함께 기록합니다.
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ import { firstLineTitle, inferFlushLevelFromEnvelope, inferTargetLayerFromIntent
|
|
|
21
21
|
import { resolveCanonicalPath } from './resolve_target.mjs';
|
|
22
22
|
import { folderForSourceType, makeEnvelope, readArgInput, renderCaptureMarkdown, slugify } from './envelope.mjs';
|
|
23
23
|
import { withFileLock, atomicWriteJson } from './durable.mjs';
|
|
24
|
+
import { renderSave } from './render.mjs';
|
|
24
25
|
import { stageAndPromote } from './stage.mjs';
|
|
25
26
|
import { loadSessionState, pendingCountForSession } from './vault_state.mjs';
|
|
26
27
|
import { appendWikiLog } from './wiki_log.mjs';
|
|
@@ -242,27 +243,7 @@ export async function captureCommand(args) {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
function done(args, payload) {
|
|
246
|
+
payload.rendered = renderSave(payload);
|
|
245
247
|
if (args.json) return { json: true, payload };
|
|
246
|
-
|
|
247
|
-
if (payload.refused) {
|
|
248
|
-
const lines = [`⛔ BLOCKED (secret): ${payload.reason}`];
|
|
249
|
-
for (const h of payload.hits || []) lines.push(` [${h.confidence}] ${h.type}: ${h.sample}`);
|
|
250
|
-
return { text: lines.join('\n') };
|
|
251
|
-
}
|
|
252
|
-
return { text: `# Capture\n\n- ${payload.error}` };
|
|
253
|
-
}
|
|
254
|
-
if (payload.dry_run) {
|
|
255
|
-
const e = payload.envelope;
|
|
256
|
-
const lines = [`# Capture Dry Run`, '', `- Source ID: ${e.source_id}`, `- Raw path: ${e.raw_path}`, `- Areas: ${e.area_candidates.join(', ') || 'none'}`, `- Sensitivity: ${e.sensitivity}`, `- Intent: ${e.write_intent}`, `- Requires user decision: ${e.requires_user_decision ? 'yes' : 'no'}`];
|
|
257
|
-
if (payload.duplicate_of) lines.push(`- Duplicate of: ${payload.duplicate_of.source_id} (${payload.duplicate_of.raw_path})`);
|
|
258
|
-
if (e.extracted_text_chars != null) lines.push(`- Extracted text: ${e.extracted_text_chars} chars via ${e.extracted_method}`);
|
|
259
|
-
if (e.overlap_candidates) lines.push(`- Overlaps existing: ${e.overlap_candidates.map((o) => o.path).join(', ')}`);
|
|
260
|
-
if (e.numeric_conflict_candidates) lines.push(`- Numeric review (${e.numeric_conflict_candidates.length}): ${e.numeric_conflict_candidates.slice(0, 3).map((n) => `${n.label} new=${n.new_values.join('/')} vs existing=${n.existing_value}`).join('; ')}`);
|
|
261
|
-
return { text: lines.join('\n') };
|
|
262
|
-
}
|
|
263
|
-
const lines = ['# Captured', '', `- Source ID: ${payload.source_id}`, `- Raw path: ${payload.raw_path}`];
|
|
264
|
-
if (payload.asset_path) lines.push(`- Raw asset: ${payload.asset_path}`);
|
|
265
|
-
lines.push(`- Envelope: ${payload.envelope_path}`, `- Requires user decision: ${payload.requires_user_decision ? 'yes' : 'no'}`);
|
|
266
|
-
if (payload.session_state_delta) lines.push(`- Session-state delta (unapplied, pending_count=${payload.session_state_delta.patch.pending_count}) returned for the vault shuttle.`);
|
|
267
|
-
return { text: lines.join('\n') };
|
|
248
|
+
return { text: payload.rendered };
|
|
268
249
|
}
|
|
@@ -10,6 +10,7 @@ import { absPath, timestamp } from './fs.mjs';
|
|
|
10
10
|
import { vaultConfig } from './config.mjs';
|
|
11
11
|
import { resolveCanonicalPath } from './resolve_target.mjs';
|
|
12
12
|
import { readJsonSafe, readJsonl } from './vault_state.mjs';
|
|
13
|
+
import { renderCompile } from './render.mjs';
|
|
13
14
|
|
|
14
15
|
function manifestEntries(value) {
|
|
15
16
|
return Object.values((value && value.files) || {});
|
|
@@ -310,32 +311,7 @@ export async function compileCommand(args) {
|
|
|
310
311
|
: 'No safe compile target was selected. This desk shows candidates and does not read raw bodies.',
|
|
311
312
|
};
|
|
312
313
|
|
|
314
|
+
payload.rendered = renderCompile(payload);
|
|
313
315
|
if (args.json) return { json: true, payload };
|
|
314
|
-
return { text:
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function render(value) {
|
|
318
|
-
const lines = ['# Neurain Compile Desk', ''];
|
|
319
|
-
lines.push(`- Mode: ${value.mode}`);
|
|
320
|
-
lines.push(`- Pending queue: ${value.queue.pending_count}`);
|
|
321
|
-
lines.push(`- Safe queue: ${value.queue.safe_count}`);
|
|
322
|
-
lines.push(`- Needs confirmation: ${value.queue.needs_confirmation_count}`);
|
|
323
|
-
lines.push(`- Deep compile candidates: ${value.source_digest.deep_compile_candidate_count}`);
|
|
324
|
-
lines.push(`- Raw full reads allowed: ${value.performance_policy.raw_full_reads_allowed}`);
|
|
325
|
-
if (value.candidates && value.candidates.length) {
|
|
326
|
-
lines.push('', `## Candidates (priority order): ${value.candidates.length}`);
|
|
327
|
-
for (const c of value.candidates) {
|
|
328
|
-
lines.push(` ${c.rank}. [${c.priority}] ${c.title || c.source_id || c.kind}${c.source_id ? ` (${c.source_id})` : ''}`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (value.excluded && value.excluded.length) {
|
|
332
|
-
lines.push('', `제외(확인필요/비공개) ${value.excluded.length}건: ${value.excluded.map((e) => e.title || e.source_id).join(', ')}`);
|
|
333
|
-
}
|
|
334
|
-
if (value.candidates && value.candidates.length) {
|
|
335
|
-
lines.push('', '몇 건 정리할까요? (숫자 1–10; "전부"는 안전한 것 최대 10건 + 나머지 개수 공시)');
|
|
336
|
-
} else if (!value.selected_target && value.excluded && value.excluded.length) {
|
|
337
|
-
lines.push('', '정리 가능한 안전 후보가 없습니다. 위 제외 항목은 확인/승인 후에만 처리됩니다.');
|
|
338
|
-
}
|
|
339
|
-
lines.push(`- Next action: ${value.next_action}`);
|
|
340
|
-
return lines.join('\n');
|
|
316
|
+
return { text: payload.rendered };
|
|
341
317
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Single source of truth for user-facing command rendering.
|
|
2
|
+
//
|
|
3
|
+
// Every five-command output (and the read-only views) is produced HERE as one
|
|
4
|
+
// deterministic markdown block, then exposed by each command-function as
|
|
5
|
+
// `payload.rendered` AND `{ text }` (the digest.mjs pattern). The CLI, the MCP
|
|
6
|
+
// server, and the vault shuttles all relay that SAME string verbatim, so the
|
|
7
|
+
// output is byte-identical across every host (Claude Code, Codex, Gemini).
|
|
8
|
+
//
|
|
9
|
+
// Rules that keep the bytes host-identical and clean:
|
|
10
|
+
// - pure functions (payload -> string), no Date.now / no I/O. Timestamps and
|
|
11
|
+
// ids arrive already stamped in the payload.
|
|
12
|
+
// - GitHub-flavored markdown tables only, ASCII characters only. Status is a
|
|
13
|
+
// word (ok / warn / missing / blocked / none / yes / no), never a glyph.
|
|
14
|
+
// No status glyphs and no em/en dashes (the operating contract bans them and
|
|
15
|
+
// a determinism test rejects them).
|
|
16
|
+
// - fixed row and column order per command; an absent value renders as "-".
|
|
17
|
+
|
|
18
|
+
const BANNED = /[✓✗⚠⛔•∞—–]/; // checks/warn/stop/bullet/inf/em/en dash
|
|
19
|
+
|
|
20
|
+
export function escapeCell(v) {
|
|
21
|
+
return String(v ?? '').replace(/\r?\n/g, ' ').replace(/(?<!\\)\|/g, '\\|').trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mdTable(headers, rows) {
|
|
25
|
+
const head = `| ${headers.join(' | ')} |`;
|
|
26
|
+
const sep = `| ${headers.map(() => '---').join(' | ')} |`;
|
|
27
|
+
const body = rows.map((r) => `| ${r.map(escapeCell).join(' | ')} |`);
|
|
28
|
+
return [head, sep, ...body].join('\n');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Two-column "field/value" table. pairs: [[label, value], ...]; absent -> "-".
|
|
32
|
+
export function fieldsTable(pairs) {
|
|
33
|
+
return mdTable(['항목', '값'], pairs.map(([k, v]) => [k, v === undefined || v === null || v === '' ? '-' : v]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// title -> "# title"; fields -> 2-col table; sections -> "## title" + table|text;
|
|
37
|
+
// notes -> trailing lines. Returns a string with no trailing newline (the CLI/print
|
|
38
|
+
// layer appends one).
|
|
39
|
+
export function dashboard({ title, fields = [], sections = [], notes = [] }) {
|
|
40
|
+
const parts = [`# ${title}`];
|
|
41
|
+
if (fields.length) parts.push('', fieldsTable(fields));
|
|
42
|
+
for (const s of sections) {
|
|
43
|
+
parts.push('', `## ${s.title}`);
|
|
44
|
+
if (s.table) parts.push('', mdTable(s.table.headers, s.table.rows));
|
|
45
|
+
else if (s.text) parts.push('', s.text);
|
|
46
|
+
}
|
|
47
|
+
for (const n of notes) parts.push('', n);
|
|
48
|
+
return parts.join('\n');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Throw if a rendered block contains a banned glyph/dash; used by renderers in a
|
|
52
|
+
// final guard and by the determinism test. Keeps the ASCII-table contract enforced.
|
|
53
|
+
export function assertClean(text) {
|
|
54
|
+
const m = String(text).match(BANNED);
|
|
55
|
+
if (m) throw new Error(`render: banned character ${JSON.stringify(m[0])} in output`);
|
|
56
|
+
return text;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const yn = (v) => (v ? 'yes' : 'no');
|
|
60
|
+
|
|
61
|
+
// ---- _compile (engine compile_desk payload) ----
|
|
62
|
+
export function renderCompile(p) {
|
|
63
|
+
const fields = [
|
|
64
|
+
['mode', p.mode],
|
|
65
|
+
['작업 대기', p.queue?.pending_count ?? 0],
|
|
66
|
+
['안전(safe)', p.queue?.safe_count ?? 0],
|
|
67
|
+
['확인 필요', p.queue?.needs_confirmation_count ?? 0],
|
|
68
|
+
['deep 후보', p.source_digest?.deep_compile_candidate_count ?? 0],
|
|
69
|
+
['raw 읽기 허용', yn(p.performance_policy?.raw_full_reads_allowed)],
|
|
70
|
+
['다음 행동', p.next_action],
|
|
71
|
+
];
|
|
72
|
+
const sections = [];
|
|
73
|
+
if (p.candidates?.length) {
|
|
74
|
+
sections.push({
|
|
75
|
+
title: `후보 (우선순위) ${p.candidates.length}건`,
|
|
76
|
+
table: {
|
|
77
|
+
headers: ['순위', '우선순위', '후보', 'source id'],
|
|
78
|
+
rows: p.candidates.map((c) => [c.rank, c.priority, c.title || c.kind || '-', c.source_id || '-']),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const notes = [];
|
|
83
|
+
if (p.excluded?.length) notes.push(`제외(확인필요/비공개) ${p.excluded.length}건: ${p.excluded.map((e) => e.title || e.source_id).join(', ')}`);
|
|
84
|
+
if (p.candidates?.length) notes.push('몇 건 정리할까요? (숫자 1-10; "전부"는 안전한 것 최대 10건)');
|
|
85
|
+
else if (!p.selected_target && p.excluded?.length) notes.push('정리 가능한 안전 후보가 없습니다. 위 제외 항목은 확인/승인 후에만 처리됩니다.');
|
|
86
|
+
return assertClean(dashboard({ title: 'Neurain Compile', fields, sections, notes }));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---- _sync (engine sync payload) ----
|
|
90
|
+
export function renderSync(p) {
|
|
91
|
+
if (!p.ok) return assertClean(`# Neurain Sync\n\n- ${p.error || p.reason}`);
|
|
92
|
+
const fields = [
|
|
93
|
+
['세션', p.session_id],
|
|
94
|
+
['mode', p.mode],
|
|
95
|
+
['pulsed', yn(p.pulsed)],
|
|
96
|
+
['작업 대기', p.queue?.pending_count ?? 0],
|
|
97
|
+
['안전(safe)', p.queue?.safe_count ?? 0],
|
|
98
|
+
['확인 필요', p.queue?.needs_review_count ?? 0],
|
|
99
|
+
['충돌', p.queue?.conflict_count ?? 0],
|
|
100
|
+
['durable wiki 쓰기', 'no'],
|
|
101
|
+
['다음 권장', p.recommendation],
|
|
102
|
+
];
|
|
103
|
+
return assertClean(dashboard({ title: 'Neurain Sync', fields }));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- _save (engine capture_durable payload) ----
|
|
107
|
+
export function renderSave(p) {
|
|
108
|
+
if (!p.ok) {
|
|
109
|
+
if (p.refused) {
|
|
110
|
+
const notes = [`secret 차단(blocked): ${p.reason}`, ...(p.hits || []).map((h) => ` [${h.confidence}] ${h.type}: ${h.sample}`)];
|
|
111
|
+
return assertClean(dashboard({ title: 'Neurain Capture - blocked', notes }));
|
|
112
|
+
}
|
|
113
|
+
return assertClean(`# Neurain Capture\n\n- ${p.error}`);
|
|
114
|
+
}
|
|
115
|
+
if (p.dry_run) {
|
|
116
|
+
const e = p.envelope;
|
|
117
|
+
const fields = [
|
|
118
|
+
['source id', e.source_id],
|
|
119
|
+
['raw 경로', e.raw_path],
|
|
120
|
+
['영역', e.area_candidates?.join(', ') || 'none'],
|
|
121
|
+
['민감도', e.sensitivity],
|
|
122
|
+
['intent', e.write_intent],
|
|
123
|
+
['확인 필요', yn(e.requires_user_decision)],
|
|
124
|
+
];
|
|
125
|
+
if (p.duplicate_of) fields.push(['중복', `${p.duplicate_of.source_id} (${p.duplicate_of.raw_path})`]);
|
|
126
|
+
if (e.extracted_text_chars != null) fields.push(['추출 글자수', `${e.extracted_text_chars} (${e.extracted_method})`]);
|
|
127
|
+
if (e.overlap_candidates) fields.push(['overlap', e.overlap_candidates.map((o) => o.path).join(', ')]);
|
|
128
|
+
if (e.numeric_conflict_candidates) fields.push(['numeric 검토', `${e.numeric_conflict_candidates.length}건`]);
|
|
129
|
+
return assertClean(dashboard({ title: 'Neurain Capture (dry run)', fields }));
|
|
130
|
+
}
|
|
131
|
+
const fields = [
|
|
132
|
+
['source id', p.source_id],
|
|
133
|
+
['raw 경로', p.raw_path],
|
|
134
|
+
];
|
|
135
|
+
if (p.asset_path) fields.push(['raw asset', p.asset_path]);
|
|
136
|
+
fields.push(['envelope', p.envelope_path], ['확인 필요', yn(p.requires_user_decision)]);
|
|
137
|
+
return assertClean(dashboard({ title: 'Neurain Captured', fields }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- _status (normalized view; both the engine status command and the vault
|
|
141
|
+
// session-status tool build this view from their own payloads) ----
|
|
142
|
+
// view = { mode: 'boot'|'all'|'session', ...fields, sections?, notes? }
|
|
143
|
+
export function renderStatus(view) {
|
|
144
|
+
if (view.mode === 'all') {
|
|
145
|
+
return assertClean(dashboard({
|
|
146
|
+
title: 'Neurain Session Status',
|
|
147
|
+
fields: [['세션 수', view.session_count ?? 0]],
|
|
148
|
+
sections: view.sessions?.length
|
|
149
|
+
? [{ title: '세션', table: { headers: ['세션', '상태', 'freshness', '작업 대기', 'handoff'], rows: view.sessions.map((s) => [s.session_id, s.status, s.freshness_status, s.pending_count, s.handoff_exists ? 'ok' : 'missing']) } }]
|
|
150
|
+
: [],
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
if (view.mode === 'session') {
|
|
154
|
+
const fields = [
|
|
155
|
+
['세션', view.session_id],
|
|
156
|
+
['영역', view.area],
|
|
157
|
+
['scope', view.scope],
|
|
158
|
+
['작업 대기', view.pending_count ?? 0],
|
|
159
|
+
['handoff', view.handoff_exists ? view.handoff_path : 'missing'],
|
|
160
|
+
['area brief', view.area_brief_exists ? view.area_brief_path : 'missing'],
|
|
161
|
+
['freshness', view.freshness_message],
|
|
162
|
+
['다음 권장', view.next || '-'],
|
|
163
|
+
];
|
|
164
|
+
return assertClean(dashboard({ title: 'Neurain Session Status', fields, notes: view.notes || [] }));
|
|
165
|
+
}
|
|
166
|
+
// boot
|
|
167
|
+
const fields = [
|
|
168
|
+
['상태', view.state || 'ok'],
|
|
169
|
+
['세션', view.session_count != null ? `${view.session_count}${view.fresh_count != null ? ` (fresh ${view.fresh_count} / stale ${view.stale_count})` : ''}` : '-'],
|
|
170
|
+
['영역', view.areas?.length ? view.areas.join(', ') : '-'],
|
|
171
|
+
['작업 대기', view.pending_text || (view.pending_count ? `${view.pending_count}` : '없음')],
|
|
172
|
+
['지식화 후보', view.compile_candidate_count ?? 0],
|
|
173
|
+
['정리 후보', view.tidy_candidate_count ?? 0],
|
|
174
|
+
['구조 점검', view.structure_findings ?? 0],
|
|
175
|
+
['지금 할 일', view.next || '-'],
|
|
176
|
+
];
|
|
177
|
+
const notes = [...(view.notes || []), '자세한 점검: _status detail'];
|
|
178
|
+
return assertClean(dashboard({ title: 'Neurain Status', fields, notes }));
|
|
179
|
+
}
|
package/src/core/status.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { vaultConfig } from './config.mjs';
|
|
|
13
13
|
import { areaBriefPath, extractMarkdownSection, freshnessFor, loadSessionState, pendingQueueRows, readJsonl, readTextAt } from './vault_state.mjs';
|
|
14
14
|
import { compileCandidateSummary } from './source_digest.mjs';
|
|
15
15
|
import { computeDigest, renderDigest } from './digest.mjs';
|
|
16
|
+
import { renderStatus } from './render.mjs';
|
|
16
17
|
import { listLessons } from './lessons.mjs';
|
|
17
18
|
|
|
18
19
|
function summarizeSession(root, vaultCfg, session, { staleDays, now }) {
|
|
@@ -108,28 +109,28 @@ export async function statusCommand(args) {
|
|
|
108
109
|
|
|
109
110
|
if (!sessionId && !args.all) {
|
|
110
111
|
const payload = bootStatus(root, vaultCfg, state, { staleDays, now });
|
|
112
|
+
payload.rendered = renderStatus({
|
|
113
|
+
mode: 'boot',
|
|
114
|
+
state: payload.ok ? 'ok' : '확인 필요',
|
|
115
|
+
session_count: payload.session_count,
|
|
116
|
+
areas: payload.active_areas,
|
|
117
|
+
pending_text: payload.total_pending_count
|
|
118
|
+
? `${payload.total_pending_count}${payload.pending_requires_confirmation_count ? ` (승인 필요 ${payload.pending_requires_confirmation_count})` : ''}`
|
|
119
|
+
: '없음',
|
|
120
|
+
compile_candidate_count: payload.compile_candidate_count || 0,
|
|
121
|
+
next: payload.next_action,
|
|
122
|
+
notes: payload.stale_guidance ? [`참고: 오래된 세션 ${payload.stale_guidance.count}개는 오류가 아닙니다.`] : [],
|
|
123
|
+
});
|
|
111
124
|
if (args.json) return { json: true, payload };
|
|
112
|
-
return {
|
|
113
|
-
text: [
|
|
114
|
-
'# Neurain Status',
|
|
115
|
-
'',
|
|
116
|
-
`- OK: ${payload.ok ? 'yes' : 'needs attention'}`,
|
|
117
|
-
`- Sessions: ${payload.session_count} | Areas: ${payload.active_areas.join(', ') || 'none'}`,
|
|
118
|
-
`- Pending: ${payload.total_pending_count}${payload.pending_requires_confirmation_count ? ` (confirmation needed: ${payload.pending_requires_confirmation_count})` : ''}`,
|
|
119
|
-
payload.compile_candidate_count ? `- Compile candidates: ${payload.compile_candidate_count}` : '',
|
|
120
|
-
payload.stale_guidance ? `- Stale sessions: ${payload.stale_guidance.count} (not an error)` : '',
|
|
121
|
-
`- Next: ${payload.next_action}`,
|
|
122
|
-
].filter(Boolean).join('\n'),
|
|
123
|
-
};
|
|
125
|
+
return { text: payload.rendered };
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
if (args.all) {
|
|
127
129
|
const sessions = Object.values(state.sessions || {}).map((s) => summarizeSession(root, vaultCfg, s, { staleDays, now }));
|
|
128
130
|
const payload = { ok: true, command: 'status', durable_write: false, mode: 'all', updated_at: state.updated_at || '', count: sessions.length, sessions };
|
|
131
|
+
payload.rendered = renderStatus({ mode: 'all', session_count: payload.count, sessions });
|
|
129
132
|
if (args.json) return { json: true, payload };
|
|
130
|
-
return {
|
|
131
|
-
text: ['# Session Status', '', `- Sessions: ${payload.count}`, ...sessions.map((s) => `- ${s.session_id}: ${s.status}, freshness ${s.freshness_status}, pending ${s.pending_count}, handoff ${s.handoff_exists ? 'ok' : 'missing'}`)].join('\n'),
|
|
132
|
-
};
|
|
133
|
+
return { text: payload.rendered };
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
const session = state.sessions[String(sessionId)];
|
|
@@ -158,18 +159,20 @@ export async function statusCommand(args) {
|
|
|
158
159
|
lesson_review: lessonReview(root),
|
|
159
160
|
};
|
|
160
161
|
// The engine never acks; a vault shim acks after delivering the rendered text.
|
|
162
|
+
const digestBlock = renderDigest(sinceLastVisit);
|
|
163
|
+
payload.rendered = renderStatus({
|
|
164
|
+
mode: 'session',
|
|
165
|
+
session_id: payload.session_id,
|
|
166
|
+
area: payload.area,
|
|
167
|
+
scope: payload.scope,
|
|
168
|
+
pending_count: payload.pending_count,
|
|
169
|
+
handoff_exists: payload.handoff_exists,
|
|
170
|
+
handoff_path: payload.handoff_path,
|
|
171
|
+
area_brief_exists: payload.area_brief_exists,
|
|
172
|
+
area_brief_path: payload.area_brief_path,
|
|
173
|
+
freshness_message: payload.freshness_message,
|
|
174
|
+
notes: digestBlock ? [digestBlock] : [],
|
|
175
|
+
});
|
|
161
176
|
if (args.json) return { json: true, payload };
|
|
162
|
-
return {
|
|
163
|
-
text: [
|
|
164
|
-
'# Session Status',
|
|
165
|
-
'',
|
|
166
|
-
`- Session: ${payload.session_id}`,
|
|
167
|
-
`- Area: ${payload.area}`,
|
|
168
|
-
`- Pending: ${payload.pending_count}`,
|
|
169
|
-
`- Handoff: ${payload.handoff_exists ? payload.handoff_path : 'missing'}`,
|
|
170
|
-
`- Area brief: ${payload.area_brief_exists ? payload.area_brief_path : 'missing'}`,
|
|
171
|
-
`- Freshness: ${payload.freshness_message}`,
|
|
172
|
-
renderDigest(sinceLastVisit) ? `\n${renderDigest(sinceLastVisit)}` : '',
|
|
173
|
-
].filter(Boolean).join('\n'),
|
|
174
|
-
};
|
|
177
|
+
return { text: payload.rendered };
|
|
175
178
|
}
|
package/src/core/sync.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { vaultConfig } from './config.mjs';
|
|
|
10
10
|
import { areaBriefPath, loadSessionState, readJsonl } from './vault_state.mjs';
|
|
11
11
|
import { slugify } from './envelope.mjs';
|
|
12
12
|
import { pulseSession } from './session_write.mjs';
|
|
13
|
+
import { renderSync } from './render.mjs';
|
|
13
14
|
|
|
14
15
|
function readSummary(args, root) {
|
|
15
16
|
if (typeof args.summary === 'string') return args.summary;
|
|
@@ -133,8 +134,7 @@ export async function syncCommand(args) {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
function done(args, payload) {
|
|
137
|
+
payload.rendered = renderSync(payload);
|
|
136
138
|
if (args.json) return { json: true, payload };
|
|
137
|
-
|
|
138
|
-
const lines = ['# Neurain Sync', '', `- Session: ${payload.session_id}`, `- Mode: ${payload.mode}`, `- Pulsed: ${payload.pulsed ? 'yes' : 'no'}`, `- Pending: ${payload.queue.pending_count}`, `- Safe: ${payload.queue.safe_count}`, `- Needs review: ${payload.queue.needs_review_count}`, `- Conflicts: ${payload.queue.conflict_count}`, `- Durable wiki writes: no`, `- Recommendation: ${payload.recommendation}`];
|
|
139
|
-
return { text: lines.join('\n') };
|
|
139
|
+
return { text: payload.rendered };
|
|
140
140
|
}
|
package/src/mcp/server.mjs
CHANGED
|
@@ -739,11 +739,11 @@ export async function startMcpServer(args) {
|
|
|
739
739
|
}
|
|
740
740
|
if (name === 'neurain_session_status') {
|
|
741
741
|
const out = await statusCommand({ _: [scopedRoot(input.root, root)], 'session-id': input.sessionId, all: input.all, 'stale-days': input.staleDays, json: true });
|
|
742
|
-
return
|
|
742
|
+
return renderedResult(out.payload);
|
|
743
743
|
}
|
|
744
744
|
if (name === 'neurain_digest_preview') {
|
|
745
745
|
const out = await digestCommand({ _: [scopedRoot(input.root, root)], session: input.session, 'window-hours': input.windowHours, json: true });
|
|
746
|
-
return
|
|
746
|
+
return renderedResult(out.payload);
|
|
747
747
|
}
|
|
748
748
|
if (name === 'neurain_queue_view') {
|
|
749
749
|
const out = await queueCommand({ _: [scopedRoot(input.root, root)], queue: input.queue, status: input.status, pending: input.pending, stats: input.stats, 'lossless-check': input.losslessCheck, json: true });
|
|
@@ -771,7 +771,7 @@ export async function startMcpServer(args) {
|
|
|
771
771
|
}
|
|
772
772
|
if (name === 'neurain_compile') {
|
|
773
773
|
const out = await compileCommand({ _: [scopedRoot(input.root, root)], target: input.target, 'session-id': input.sessionId, top: input.top, manifest: input.manifest, queue: input.queue, json: true });
|
|
774
|
-
return
|
|
774
|
+
return renderedResult(out.payload);
|
|
775
775
|
}
|
|
776
776
|
throw new Error(`Unknown Neurain tool: ${name}`);
|
|
777
777
|
});
|
|
@@ -837,6 +837,14 @@ function textResult(value) {
|
|
|
837
837
|
};
|
|
838
838
|
}
|
|
839
839
|
|
|
840
|
+
// User-facing command views relay the engine's single rendered block (payload.rendered)
|
|
841
|
+
// verbatim, so an MCP host shows the SAME markdown as the CLI. Falls back to JSON only
|
|
842
|
+
// if a payload has no rendered block. Diagnostic/eval tools keep textResult (JSON).
|
|
843
|
+
function renderedResult(payload) {
|
|
844
|
+
const text = payload?.rendered ?? JSON.stringify(payload, null, 2);
|
|
845
|
+
return { content: [{ type: 'text', text }] };
|
|
846
|
+
}
|
|
847
|
+
|
|
840
848
|
function answerEvalScaledCounts(total) {
|
|
841
849
|
const count = Math.max(10, Math.min(Number(total || 120), 500));
|
|
842
850
|
const supportedCases = Math.max(1, Math.round(count * 0.42));
|