nexus-prime 7.9.13 → 7.9.15

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.
Files changed (32) hide show
  1. package/dist/agents/adapters/mcp/stdio-buffer.d.ts +6 -0
  2. package/dist/agents/adapters/mcp/stdio-buffer.js +45 -0
  3. package/dist/agents/adapters/mcp.d.ts +2 -0
  4. package/dist/agents/adapters/mcp.js +60 -7
  5. package/dist/cli/install-wizard.js +19 -0
  6. package/dist/cli.js +31 -1
  7. package/dist/dashboard/app/index.html +8 -0
  8. package/dist/dashboard/app/main.js +7 -0
  9. package/dist/dashboard/app/state.js +5 -0
  10. package/dist/dashboard/app/styles/board.css +163 -2
  11. package/dist/dashboard/app/styles/context-log.css +167 -0
  12. package/dist/dashboard/app/styles/memory.css +63 -0
  13. package/dist/dashboard/app/styles/workforce.css +21 -0
  14. package/dist/dashboard/app/views/board.js +145 -7
  15. package/dist/dashboard/app/views/context-log.js +158 -0
  16. package/dist/dashboard/app/views/memory.js +87 -3
  17. package/dist/dashboard/app/views/workforce.js +22 -6
  18. package/dist/dashboard/routes/events.js +80 -3
  19. package/dist/dashboard/stream/sse-broker.js +25 -13
  20. package/dist/engines/client-bootstrap.js +66 -20
  21. package/dist/engines/code-review-graph-client.d.ts +11 -3
  22. package/dist/engines/code-review-graph-client.js +151 -24
  23. package/dist/engines/instruction-gateway.js +6 -0
  24. package/dist/engines/mcp-entrypoint.js +3 -1
  25. package/dist/engines/orchestrator/decision-spine.d.ts +170 -0
  26. package/dist/engines/orchestrator/decision-spine.js +424 -0
  27. package/dist/engines/orchestrator/selection-policy.d.ts +39 -0
  28. package/dist/engines/orchestrator/selection-policy.js +32 -0
  29. package/dist/engines/orchestrator.js +19 -33
  30. package/dist/phantom/runtime.d.ts +16 -0
  31. package/dist/phantom/runtime.js +158 -20
  32. package/package.json +2 -2
@@ -0,0 +1,6 @@
1
+ import { PassThrough } from 'stream';
2
+ export declare function primeMcpStdioInput(): void;
3
+ export declare function getMcpStdioInput(): PassThrough | (NodeJS.ReadStream & {
4
+ fd: 0;
5
+ });
6
+ export declare function flushPrimedMcpStdioInput(): void;
@@ -0,0 +1,45 @@
1
+ import { PassThrough } from 'stream';
2
+ let primedStdioInput = null;
3
+ let primedStdioInputStarted = false;
4
+ let primedStdioInputReady = false;
5
+ let primedStdioInputEnded = false;
6
+ const primedStdioChunks = [];
7
+ export function primeMcpStdioInput() {
8
+ if (primedStdioInputStarted || process.stdin.isTTY)
9
+ return;
10
+ primedStdioInputStarted = true;
11
+ process.stdin.on('data', (chunk) => {
12
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
13
+ if (primedStdioInput && primedStdioInputReady) {
14
+ primedStdioInput.write(buffer);
15
+ return;
16
+ }
17
+ primedStdioChunks.push(buffer);
18
+ });
19
+ process.stdin.on('end', () => {
20
+ primedStdioInputEnded = true;
21
+ if (primedStdioInput && primedStdioInputReady)
22
+ primedStdioInput.end();
23
+ });
24
+ process.stdin.on('error', (error) => primedStdioInput?.destroy(error));
25
+ process.stdin.resume();
26
+ }
27
+ export function getMcpStdioInput() {
28
+ if (primedStdioInputStarted && !primedStdioInput) {
29
+ primedStdioInput = new PassThrough();
30
+ }
31
+ return primedStdioInput ?? process.stdin;
32
+ }
33
+ export function flushPrimedMcpStdioInput() {
34
+ if (!primedStdioInput)
35
+ return;
36
+ primedStdioInputReady = true;
37
+ for (const chunk of primedStdioChunks.splice(0)) {
38
+ primedStdioInput.write(chunk);
39
+ }
40
+ if (primedStdioInputEnded)
41
+ primedStdioInput.end();
42
+ }
43
+ if (process.argv[2] === 'mcp') {
44
+ primeMcpStdioInput();
45
+ }
@@ -20,6 +20,7 @@ export declare class MCPAdapter implements Adapter {
20
20
  private currentTask;
21
21
  private middlewarePipeline;
22
22
  private scanCache;
23
+ private sessionLifecycleFinalized;
23
24
  private sciFiMatrixLog;
24
25
  private classifyToolCategory;
25
26
  private sciFiToolHeader;
@@ -83,6 +84,7 @@ export declare class MCPAdapter implements Adapter {
83
84
  private extractFileRefsFromArgs;
84
85
  scanSourceFiles(cwd: string): Promise<string[]>;
85
86
  connect(): Promise<void>;
87
+ private finalizeSessionLifecycle;
86
88
  disconnect(): Promise<void>;
87
89
  send(message: NetworkMessage): Promise<void>;
88
90
  receive(message: NetworkMessage): void;
@@ -30,6 +30,7 @@ import { getFallbackRuntime, getFallbackOrchestrator, getGuardrailEngine, normal
30
30
  import { dispatchMcpToolCall } from './mcp/dispatch.js';
31
31
  import { LifecyclePolicy } from '../../engines/lifecycle-policy.js';
32
32
  import { startWatchdog } from './mcp/watchdog.js';
33
+ import { getMcpStdioInput } from './mcp/stdio-buffer.js';
33
34
  // Derive project root from this file's location (dist/agents/adapters/mcp.js → project root)
34
35
  const __filename = fileURLToPath(import.meta.url);
35
36
  const PROJECT_ROOT = path.resolve(path.dirname(__filename), '..', '..', '..');
@@ -159,6 +160,7 @@ export class MCPAdapter {
159
160
  currentTask = '';
160
161
  middlewarePipeline;
161
162
  scanCache = new Map();
163
+ sessionLifecycleFinalized = false;
162
164
  sciFiMatrixLog(title, metrics, intent) {
163
165
  const width = 76;
164
166
  console.error(`\n\x1b[36m╔═══ [ \x1b[37m\x1b[1mNEXUS PRIME · ORCHESTRATION MATRIX\x1b[0m\x1b[36m ] ${'═'.repeat(Math.max(0, width - 48))}╗\x1b[0m`);
@@ -875,6 +877,9 @@ export class MCPAdapter {
875
877
  }
876
878
  else {
877
879
  this.telemetry.observeSuccessfulToolCall(toolName, args);
880
+ if (toolName === 'nexus_session_dna' && String(args.action ?? 'load') === 'generate') {
881
+ this.sessionLifecycleFinalized = true;
882
+ }
878
883
  }
879
884
  }
880
885
  // Surface meaningful tool calls on the SSE feed; persist only high-signal outcomes.
@@ -1152,28 +1157,76 @@ export class MCPAdapter {
1152
1157
  return scanSourceFilesUtil(cwd, this.scanCache);
1153
1158
  }
1154
1159
  async connect() {
1155
- const transport = new StdioServerTransport();
1160
+ const transport = new StdioServerTransport(getMcpStdioInput(), process.stdout);
1156
1161
  await this.server.connect(transport);
1157
1162
  this.connected = true;
1158
1163
  startWatchdog();
1159
1164
  console.error('[MCP Adapter] Connected — runtime tools active');
1160
1165
  }
1161
- async disconnect() {
1162
- // Auto-flush Session DNA on disconnect
1166
+ async finalizeSessionLifecycle(reason) {
1167
+ if (this.sessionLifecycleFinalized && reason !== 'explicit')
1168
+ return;
1169
+ const telemetry = this.telemetry.snapshot();
1170
+ try {
1171
+ const promoted = this.nexusRef?.runTierPromotion?.();
1172
+ if (promoted)
1173
+ nexusEventBus.emit('memory.tier.promoted', promoted);
1174
+ }
1175
+ catch (e) {
1176
+ nexusEventBus.emit('mcp.handler.failed', {
1177
+ toolName: 'nexus_session_dna.disconnect.tierPromotion',
1178
+ code: 'internal',
1179
+ attempts: 1,
1180
+ durationMs: 0,
1181
+ });
1182
+ console.error('[MCP Adapter] Failed to run Session DNA tier promotion:', e);
1183
+ }
1163
1184
  try {
1164
- const telemetry = this.telemetry.snapshot();
1165
1185
  this.sessionDNA.syncFromTelemetry({
1166
1186
  callCount: telemetry.callCount,
1167
1187
  memoriesStored: telemetry.memoriesStored,
1168
1188
  memoriesRecalled: telemetry.memoriesRecalled,
1169
1189
  tokensOptimized: telemetry.tokensOptimized,
1170
1190
  });
1171
- this.sessionDNA.flush();
1172
- console.error('[MCP Adapter] Session DNA flushed');
1191
+ const dna = this.sessionDNA.flush();
1192
+ this.sessionLifecycleFinalized = true;
1193
+ try {
1194
+ const runtimeTokens = this.runtime?.getUsageSnapshot().tokens?.grossInputTokens ?? 0;
1195
+ this.getOrchestrator({}).persistSessionSummary(telemetry.tokensOptimized || runtimeTokens || 0);
1196
+ }
1197
+ catch (e) {
1198
+ nexusEventBus.emit('mcp.handler.failed', {
1199
+ toolName: 'nexus_session_dna.disconnect.persistSessionSummary',
1200
+ code: 'internal',
1201
+ attempts: 1,
1202
+ durationMs: 0,
1203
+ });
1204
+ console.error('[MCP Adapter] Failed to persist Session DNA summary:', e);
1205
+ }
1206
+ await getSharedTelemetry().trackSessionEnd(this.telemetry.elapsedMs(), {
1207
+ projectId: this.nexusRef?.getWorkspaceContext?.()?.repoRoot ?? '',
1208
+ tokensSaved: telemetry.tokensOptimized,
1209
+ memoriesCreated: telemetry.memoriesStored,
1210
+ memoriesRecalled: telemetry.memoriesRecalled,
1211
+ toolsUsed: telemetry.callCount,
1212
+ sessionId: dna.sessionId,
1213
+ }).catch((e) => {
1214
+ nexusEventBus.emit('mcp.handler.failed', {
1215
+ toolName: 'nexus_session_dna.disconnect.trackSessionEnd',
1216
+ code: 'internal',
1217
+ attempts: 1,
1218
+ durationMs: 0,
1219
+ });
1220
+ console.error('[MCP Adapter] Failed to track Session DNA session end:', e);
1221
+ });
1222
+ console.error(`[MCP Adapter] Session DNA finalized on ${reason}`);
1173
1223
  }
1174
1224
  catch (e) {
1175
- console.error('[MCP Adapter] Failed to flush Session DNA:', e);
1225
+ console.error('[MCP Adapter] Failed to finalize Session DNA:', e);
1176
1226
  }
1227
+ }
1228
+ async disconnect() {
1229
+ await this.finalizeSessionLifecycle('disconnect');
1177
1230
  await this.server.close();
1178
1231
  this.connected = false;
1179
1232
  console.error('[MCP Adapter] Disconnected');
@@ -159,6 +159,21 @@ function _writeClaudeCodeHooks(workspaceRoot) {
159
159
  });
160
160
  });
161
161
  }
162
+ function _isManagedGraphPeer(server) {
163
+ if (!server || typeof server !== 'object' || Array.isArray(server))
164
+ return false;
165
+ const record = server;
166
+ const args = Array.isArray(record.args) ? record.args.map(String) : [];
167
+ return record.command === 'goatlas' && args.length === 1 && args[0] === 'mcp';
168
+ }
169
+ function _stripInternalGraphPeers(mcpServers) {
170
+ if (!mcpServers)
171
+ return;
172
+ delete mcpServers['code-review-graph'];
173
+ if (_isManagedGraphPeer(mcpServers['atlas'])) {
174
+ delete mcpServers['atlas'];
175
+ }
176
+ }
162
177
  /**
163
178
  * Write workspace-local MCP config files for IDEs whose workspace directories
164
179
  * already exist (`.vscode/`, `.cursor/`). Non-fatal; errors are swallowed.
@@ -181,6 +196,7 @@ function _writeWorkspaceLocalConfigs(callerIde, workspaceRoot) {
181
196
  ? settings.mcpServers
182
197
  : {};
183
198
  servers['nexus-prime'] = entry;
199
+ _stripInternalGraphPeers(servers);
184
200
  settings.mcpServers = servers;
185
201
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
186
202
  appendToManifest((m) => {
@@ -212,6 +228,7 @@ function _writeWorkspaceLocalConfigs(callerIde, workspaceRoot) {
212
228
  ? mcp.mcpServers
213
229
  : {};
214
230
  servers['nexus-prime'] = entry;
231
+ _stripInternalGraphPeers(servers);
215
232
  mcp.mcpServers = servers;
216
233
  writeFileSync(mcpPath, JSON.stringify(mcp, null, 2), 'utf8');
217
234
  appendToManifest((m) => {
@@ -347,6 +364,7 @@ function mergeIntoExistingConfig(configPath, newContent, ide) {
347
364
  ...(existing.mcpServers ?? {}),
348
365
  ...(incoming.mcpServers ?? {}),
349
366
  };
367
+ _stripInternalGraphPeers(existing.mcpServers);
350
368
  return JSON.stringify(existing, null, 2);
351
369
  }
352
370
  if (ide === 'claude-code') {
@@ -362,6 +380,7 @@ function mergeIntoExistingConfig(configPath, newContent, ide) {
362
380
  ...(existing.mcpServers ?? {}),
363
381
  ...(incoming.mcpServers ?? {}),
364
382
  };
383
+ _stripInternalGraphPeers(existing.mcpServers);
365
384
  }
366
385
  return JSON.stringify(existing, null, 2);
367
386
  }
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Nexus Prime CLI
4
4
  */
5
5
  import 'dotenv/config';
6
+ import { flushPrimedMcpStdioInput, primeMcpStdioInput } from './agents/adapters/mcp/stdio-buffer.js';
6
7
  import { Command } from 'commander';
7
8
  import { createNexusPrime } from './index.js';
8
9
  import { TokenSupremacyEngine, formatReadingPlan } from './engines/token-supremacy.js';
@@ -196,9 +197,25 @@ function writeStandardMcpConfig(targetPath, workspaceRoot) {
196
197
  const existing = readJson(targetPath);
197
198
  existing.mcpServers = existing.mcpServers ?? {};
198
199
  existing.mcpServers['nexus-prime'] = buildStandardMcpServerConfig(workspaceRoot);
200
+ delete existing.mcpServers['code-review-graph'];
201
+ if (isNexusManagedGraphPeer(existing.mcpServers['atlas'])) {
202
+ delete existing.mcpServers['atlas'];
203
+ }
199
204
  ensureParentDir(targetPath);
200
205
  writeFileSync(targetPath, JSON.stringify(existing, null, 2));
201
206
  }
207
+ function isNexusManagedGraphPeer(server) {
208
+ if (!server || typeof server !== 'object' || Array.isArray(server))
209
+ return false;
210
+ const record = server;
211
+ const args = Array.isArray(record['args']) ? record['args'].map(String) : [];
212
+ return record['command'] === 'goatlas' && args.length === 1 && args[0] === 'mcp';
213
+ }
214
+ function jsonExposesInternalGraphPeer(parsed) {
215
+ const servers = parsed?.mcpServers ?? parsed?.mcp ?? {};
216
+ return Boolean(servers?.['code-review-graph']
217
+ || isNexusManagedGraphPeer(servers?.['atlas']));
218
+ }
202
219
  function writeOpencodeConfig(targetPath, workspaceRoot) {
203
220
  const existing = readJson(targetPath);
204
221
  const mcpConfig = buildStandardMcpServerConfig(workspaceRoot);
@@ -427,6 +444,15 @@ function buildInstructionFiles(clientId) {
427
444
  content: artifact.content,
428
445
  }));
429
446
  }
447
+ function resolveClaudeDesktopConfigPath() {
448
+ if (process.platform === 'darwin') {
449
+ return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
450
+ }
451
+ if (process.platform === 'win32') {
452
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
453
+ }
454
+ return join(homedir(), '.claude', 'claude_desktop_config.json');
455
+ }
430
456
  function getSetupDefinition(clientId) {
431
457
  const instructionFiles = buildInstructionFiles(clientId);
432
458
  if (clientId === 'codex') {
@@ -457,7 +483,7 @@ function getSetupDefinition(clientId) {
457
483
  return {
458
484
  id: clientId,
459
485
  label: 'Claude Desktop',
460
- configPath: join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
486
+ configPath: resolveClaudeDesktopConfigPath(),
461
487
  instructionFiles,
462
488
  };
463
489
  }
@@ -582,6 +608,8 @@ function hasExpectedConfig(definition) {
582
608
  }
583
609
  try {
584
610
  const parsed = JSON.parse(readFileSync(definition.configPath, 'utf8'));
611
+ if (jsonExposesInternalGraphPeer(parsed))
612
+ return false;
585
613
  if (definition.id === 'opencode') {
586
614
  const server = parsed?.mcp?.['nexus-prime'];
587
615
  return Boolean(server && isStableNexusMcpServerConfig(server, 'environment'));
@@ -813,6 +841,7 @@ program
813
841
  .option('--standalone', 'Skip daemon detection, run in-process (debug mode)')
814
842
  .action(async (options) => {
815
843
  // stdio transport requires strict JSON-RPC on stdout — no console.log here.
844
+ primeMcpStdioInput();
816
845
  const workspaceContext = resolveWorkspaceContext({ workspaceRoot: getWorkspaceRoot() });
817
846
  const caller = detectCallerIDE();
818
847
  process.env.NEXUS_DAEMON_AUTOSPAWN ??= '0';
@@ -955,6 +984,7 @@ program
955
984
  throw new Error('MCP adapter did not become ready before the startup deadline.');
956
985
  }
957
986
  await startup;
987
+ flushPrimedMcpStdioInput();
958
988
  console.error('Nexus Prime MCP Server running on stdio (standalone)');
959
989
  console.error('Memory persistence: active (~/.nexus-prime/memory.db)');
960
990
  const shutdown = async (signal) => {
@@ -11,6 +11,7 @@
11
11
  <link rel="stylesheet" href="./styles/board.css">
12
12
  <link rel="stylesheet" href="./styles/workforce.css">
13
13
  <link rel="stylesheet" href="./styles/memory.css">
14
+ <link rel="stylesheet" href="./styles/context-log.css">
14
15
  <link rel="stylesheet" href="./styles/governance.css">
15
16
  <link rel="stylesheet" href="./styles/trust.css">
16
17
  <link rel="stylesheet" href="./styles/animation.css">
@@ -55,6 +56,7 @@ if(location.protocol==='file:'){
55
56
  <a class="nav-item" data-nav="runtime" href="#runtime">⚡ Runtime</a>
56
57
  <a class="nav-item" data-nav="workforce" href="#workforce">Workforce</a>
57
58
  <a class="nav-item" data-nav="memory" href="#memory">Memory</a>
59
+ <a class="nav-item" data-nav="context-log" href="#context-log">Context Log</a>
58
60
  <a class="nav-item" data-nav="repo" href="#repo">Repo</a>
59
61
  <a class="nav-item" data-nav="knowledge" href="#knowledge">Knowledge</a>
60
62
  <a class="nav-item" data-nav="governance" href="#governance">Governance</a>
@@ -214,12 +216,18 @@ if(location.protocol==='file:'){
214
216
  <input id="mem-search" type="text" placeholder="Search memories…" aria-label="Search memories">
215
217
  <button class="btn btn-sm" id="mem-list-browse-btn">Browse</button>
216
218
  </div>
219
+ <div id="memory-selection-panel" class="memory-selection-panel"></div>
217
220
  <div id="mem-list"></div>
218
221
  </div>
219
222
  <div class="card"><div id="mem-stats"></div></div>
220
223
  </div>
221
224
  </section>
222
225
 
226
+ <!-- CONTEXT LOG ────────────────────────────────────────────── -->
227
+ <section class="view-panel" data-view="context-log" aria-label="Context log">
228
+ <div id="context-log-view"></div>
229
+ </section>
230
+
223
231
  <!-- REPO ───────────────────────────────────────────────────── -->
224
232
  <section class="view-panel" data-view="repo" aria-label="Repo graph">
225
233
  <div class="shd">Repo knowledge graph</div>
@@ -15,6 +15,7 @@ import { init as initCmdBar } from './widgets/command-bar.js';
15
15
  import * as Board from './views/board.js';
16
16
  import * as Workforce from './views/workforce.js';
17
17
  import * as Memory from './views/memory.js';
18
+ import * as ContextLog from './views/context-log.js';
18
19
  import * as Knowledge from './views/knowledge.js';
19
20
  import * as Repo from './views/repo.js';
20
21
  import * as Governance from './views/governance.js';
@@ -53,6 +54,7 @@ navRegister('board', Board.load);
53
54
  navRegister('runtime', Runtime.load);
54
55
  navRegister('workforce', Workforce.load);
55
56
  navRegister('memory', Memory.load);
57
+ navRegister('context-log', ContextLog.load);
56
58
  navRegister('repo', Repo.load);
57
59
  navRegister('knowledge', Knowledge.load);
58
60
  navRegister('governance', Governance.load);
@@ -113,10 +115,14 @@ setOnEvent(evt => {
113
115
  // here we just refresh the surface payload so kanban + KPIs reflect the new run.
114
116
  if (String(evt.type||'').startsWith('orchestration.')) {
115
117
  bustCache('/api/dashboard/surface/operate');
118
+ bustCache('/api/runs');
116
119
  if (tab === 'board') {
117
120
  Board.handleOrchestrationEvent?.(evt);
118
121
  Board.render();
119
122
  }
123
+ if (tab === 'context-log') {
124
+ ContextLog.load();
125
+ }
120
126
  }
121
127
  // Workspace root promoted (e.g. from bootstrap hint) — refresh header immediately.
122
128
  if (String(evt.type||'') === 'workspace.changed') {
@@ -300,6 +306,7 @@ function _reloadTab(tab) {
300
306
  if (tab === 'board') Board.load();
301
307
  if (tab === 'workforce') Workforce.load();
302
308
  if (tab === 'memory') Memory.load();
309
+ if (tab === 'context-log') ContextLog.load();
303
310
  if (tab === 'knowledge') Knowledge.load();
304
311
  }
305
312
 
@@ -65,6 +65,7 @@ export const S = {
65
65
  memQuery: '',
66
66
  graphSim: null,
67
67
  memLane: 'all',
68
+ selectedMemoryIds: [],
68
69
  memorySurface: null,
69
70
  topology: null,
70
71
 
@@ -81,6 +82,10 @@ export const S = {
81
82
  // Orchestration pipeline (live decomposition + completion from SSE)
82
83
  lastDecomposition: null,
83
84
  lastCompletion: null,
85
+ lastDecisionSpine: null,
86
+ contextLogRuns: [],
87
+ contextLogSelectedRunId: null,
88
+ contextLogSpine: null,
84
89
 
85
90
  // Neural Stream HUD ring buffer (latest SSE events, restored from v3.8.0)
86
91
  neuralStream: [],
@@ -30,9 +30,11 @@
30
30
  .kb-body { display: flex; flex-direction: column; gap: 6px; min-height: 100px; }
31
31
  .kb-empty {
32
32
  height: 80px; border: 1px dashed var(--border); border-radius: var(--radius);
33
- display: flex; align-items: center; justify-content: center;
33
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
34
+ gap: 4px; padding: 0 14px; text-align: center;
34
35
  }
35
- .kb-empty-txt { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-dim); }
36
+ .kb-empty-title { font-family: var(--font-mono); font-size: 0.78rem; color: var(--text-dim); }
37
+ .kb-empty-sub { font-size: 0.72rem; line-height: 1.35; color: var(--text-dim); opacity: 0.75; }
36
38
  .kcard {
37
39
  padding: 9px 11px; background: var(--bg-elevated); border: 1px solid var(--border);
38
40
  border-radius: var(--radius); cursor: pointer;
@@ -168,6 +170,165 @@
168
170
  .ledger-n { font-family: var(--font-mono); font-size: 0.78rem; color: var(--accent); flex-shrink: 0; min-width: 18px; }
169
171
  .ledger-txt { color: var(--text-muted); }
170
172
 
173
+ /* ── Decision Spine ── */
174
+ .decision-spine-prompt {
175
+ padding: 9px 11px;
176
+ margin-bottom: 10px;
177
+ border: 1px solid var(--border);
178
+ border-radius: var(--radius);
179
+ background: var(--bg-panel);
180
+ color: var(--text-main);
181
+ font-size: 0.78rem;
182
+ line-height: 1.45;
183
+ }
184
+ .decision-spine-grid {
185
+ display: grid;
186
+ grid-template-columns: repeat(3, minmax(0, 1fr));
187
+ gap: 8px;
188
+ margin-bottom: 10px;
189
+ }
190
+ .decision-spine-card {
191
+ min-width: 0;
192
+ padding: 8px 10px;
193
+ border: 1px solid var(--border);
194
+ border-radius: var(--radius);
195
+ background: var(--bg-elevated);
196
+ }
197
+ .decision-spine-k {
198
+ font-family: var(--font-mono);
199
+ font-size: 0.68rem;
200
+ letter-spacing: 0.08em;
201
+ text-transform: uppercase;
202
+ color: var(--text-dim);
203
+ margin-bottom: 4px;
204
+ }
205
+ .decision-spine-v {
206
+ font-family: var(--font-mono);
207
+ font-size: 0.9rem;
208
+ color: var(--accent);
209
+ white-space: nowrap;
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ }
213
+ .decision-spine-sub {
214
+ margin-top: 3px;
215
+ color: var(--text-dim);
216
+ font-size: 0.72rem;
217
+ white-space: nowrap;
218
+ overflow: hidden;
219
+ text-overflow: ellipsis;
220
+ }
221
+ .decision-spine-block {
222
+ display: grid;
223
+ grid-template-columns: 68px 1fr;
224
+ gap: 8px;
225
+ align-items: start;
226
+ padding: 6px 0;
227
+ border-top: 1px solid var(--border);
228
+ font-size: 0.78rem;
229
+ }
230
+ .decision-spine-block > span {
231
+ font-family: var(--font-mono);
232
+ color: var(--text-dim);
233
+ text-transform: uppercase;
234
+ letter-spacing: 0.06em;
235
+ font-size: 0.68rem;
236
+ padding-top: 2px;
237
+ }
238
+ .decision-spine-list {
239
+ display: flex;
240
+ flex-wrap: wrap;
241
+ gap: 4px;
242
+ min-width: 0;
243
+ }
244
+ .decision-spine-latest {
245
+ margin-top: 10px;
246
+ padding: 7px 9px;
247
+ border-left: 2px solid var(--accent);
248
+ background: var(--bg-panel);
249
+ color: var(--text-muted);
250
+ font-size: 0.78rem;
251
+ line-height: 1.4;
252
+ }
253
+ .decision-spine-full-link {
254
+ float: right;
255
+ color: var(--accent);
256
+ font-family: var(--font-mono);
257
+ font-size: 0.68rem;
258
+ text-decoration: none;
259
+ }
260
+ .decision-spine-full-link:hover { text-decoration: underline; }
261
+ .decision-spine-mini {
262
+ margin-top: 8px;
263
+ padding: 8px 12px;
264
+ border-left: 3px solid var(--secondary);
265
+ background: var(--bg-panel);
266
+ }
267
+ .decision-spine-mini-head {
268
+ font-size: 0.78rem;
269
+ font-weight: 600;
270
+ margin-bottom: 5px;
271
+ }
272
+ .decision-spine-mini-row,
273
+ .decision-spine-mini-lists {
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 6px;
277
+ color: var(--text-dim);
278
+ font-family: var(--font-mono);
279
+ font-size: 0.72rem;
280
+ }
281
+ .decision-spine-mini-lists { margin-top: 6px; }
282
+ .decision-spine-browser {
283
+ display: grid;
284
+ grid-template-columns: repeat(2, minmax(0, 1fr));
285
+ gap: 10px;
286
+ margin-top: 10px;
287
+ padding-top: 10px;
288
+ border-top: 1px solid var(--border);
289
+ }
290
+ .decision-spine-browser-col {
291
+ min-width: 0;
292
+ }
293
+ .decision-spine-browser-title {
294
+ font-family: var(--font-mono);
295
+ font-size: 0.68rem;
296
+ letter-spacing: 0.06em;
297
+ text-transform: uppercase;
298
+ color: var(--text-dim);
299
+ margin-bottom: 6px;
300
+ }
301
+ .decision-spine-log-row {
302
+ min-width: 0;
303
+ padding: 7px 8px;
304
+ border: 1px solid var(--border);
305
+ border-radius: var(--radius);
306
+ background: var(--bg-panel);
307
+ margin-bottom: 6px;
308
+ }
309
+ .decision-spine-log-head {
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ gap: 8px;
314
+ color: var(--accent);
315
+ font-family: var(--font-mono);
316
+ font-size: 0.72rem;
317
+ margin-bottom: 4px;
318
+ }
319
+ .decision-spine-log-reason {
320
+ color: var(--text-muted);
321
+ font-size: 0.76rem;
322
+ line-height: 1.35;
323
+ margin-bottom: 5px;
324
+ }
325
+ .decision-spine-log-ref,
326
+ .decision-spine-empty {
327
+ color: var(--text-dim);
328
+ font-size: 0.72rem;
329
+ line-height: 1.35;
330
+ }
331
+
171
332
  /* ── Tasks Tab ── */
172
333
  .task-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
173
334
  .filter-grp { display: flex; gap: 2px; }