@nforma.ai/nforma 0.2.1 → 0.28.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/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +40 -40
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/nf-precompact.test.js +227 -0
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-prompt.test.js +698 -0
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/nf-session-start.test.js +354 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/nf-slot-correlator.test.js +85 -0
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/nf-spec-regen.test.js +73 -0
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/nf-statusline.test.js +157 -0
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/nf-stop.test.js +1388 -0
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/nf-token-collector.test.js +262 -0
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +4 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- package/hooks/dist/qgsd-session-start.js +0 -122
package/bin/nForma.cjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
|
+
const crypto = require('crypto');
|
|
7
8
|
const { spawnSync } = require('child_process');
|
|
8
9
|
|
|
9
10
|
// ─── Circuit breaker CLI (non-interactive, exits before TUI loads) ───────────
|
|
@@ -89,7 +90,7 @@ const { readClaudeJson, writeClaudeJson, getGlobalMcpServers } = core;
|
|
|
89
90
|
const {
|
|
90
91
|
buildDashboardLines, probeAllSlots,
|
|
91
92
|
maskKey, deriveKeytarAccount,
|
|
92
|
-
|
|
93
|
+
readNfJson, writeNfJson,
|
|
93
94
|
buildExportData, validateImportSchema, buildBackupPath,
|
|
94
95
|
buildTimeoutChoices, applyTimeoutUpdate,
|
|
95
96
|
buildPolicyChoices,
|
|
@@ -105,6 +106,7 @@ const reqCore = require('./requirements-core.cjs');
|
|
|
105
106
|
const CLAUDE_JSON_PATH = path.join(os.homedir(), '.claude.json');
|
|
106
107
|
const PROVIDERS_JSON = path.join(__dirname, 'providers.json');
|
|
107
108
|
const PROVIDERS_JSON_TMP = PROVIDERS_JSON + '.tmp';
|
|
109
|
+
const SESSIONS_FILE = path.join(os.homedir(), '.claude', 'nf', 'sessions.json');
|
|
108
110
|
|
|
109
111
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
110
112
|
const PROVIDER_KEY_NAMES = [
|
|
@@ -202,11 +204,37 @@ function logEvent(level, msg) {
|
|
|
202
204
|
|
|
203
205
|
if (_xtermError) logEvent('warn', `blessed-xterm unavailable: ${_xtermError}`);
|
|
204
206
|
|
|
205
|
-
// ─── Session state
|
|
206
|
-
const sessions = []; // { id, name, cwd, term (XTerm widget), alive }
|
|
207
|
+
// ─── Session state & persistence ─────────────────────────────────────────────
|
|
208
|
+
const sessions = []; // { id, name, cwd, claudeSessionId, term (XTerm widget), alive }
|
|
207
209
|
let activeSessionIdx = -1; // -1 = no terminal shown
|
|
208
210
|
let sessionIdCounter = 0;
|
|
209
211
|
|
|
212
|
+
function loadPersistedSessions() {
|
|
213
|
+
try {
|
|
214
|
+
if (!fs.existsSync(SESSIONS_FILE)) return [];
|
|
215
|
+
return JSON.parse(fs.readFileSync(SESSIONS_FILE, 'utf8'));
|
|
216
|
+
} catch (_) { return []; }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function savePersistedSessions() {
|
|
220
|
+
const data = sessions.map(s => ({
|
|
221
|
+
id: s.id, name: s.name, cwd: s.cwd, claudeSessionId: s.claudeSessionId,
|
|
222
|
+
}));
|
|
223
|
+
try {
|
|
224
|
+
fs.mkdirSync(path.dirname(SESSIONS_FILE), { recursive: true });
|
|
225
|
+
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(data, null, 2), 'utf8');
|
|
226
|
+
} catch (_) {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function removePersistedSession(claudeSessionId) {
|
|
230
|
+
try {
|
|
231
|
+
const persisted = loadPersistedSessions();
|
|
232
|
+
const filtered = persisted.filter(s => s.claudeSessionId !== claudeSessionId);
|
|
233
|
+
fs.mkdirSync(path.dirname(SESSIONS_FILE), { recursive: true });
|
|
234
|
+
fs.writeFileSync(SESSIONS_FILE, JSON.stringify(filtered, null, 2), 'utf8');
|
|
235
|
+
} catch (_) {}
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
// ─── Module switching (activity bar) ──────────────────────────────────────────
|
|
211
239
|
let activeModuleIdx = 0;
|
|
212
240
|
|
|
@@ -249,6 +277,9 @@ function switchModule(idx) {
|
|
|
249
277
|
return;
|
|
250
278
|
}
|
|
251
279
|
|
|
280
|
+
// Sessions module: don't auto-dispatch (avoids unwanted "New Session" modal)
|
|
281
|
+
if (idx === 3) return;
|
|
282
|
+
|
|
252
283
|
// Auto-show first item's content (skip separators, use view-only for interactive actions)
|
|
253
284
|
const first = mod.items[0];
|
|
254
285
|
if (first && first.action !== 'sep') {
|
|
@@ -262,8 +293,24 @@ function switchModule(idx) {
|
|
|
262
293
|
function refreshSessionMenu() {
|
|
263
294
|
const mod = MODULES[3]; // Sessions
|
|
264
295
|
const items = [{ label: ' New Session', action: 'session-new' }];
|
|
296
|
+
|
|
297
|
+
// Show persisted sessions that aren't currently loaded
|
|
298
|
+
const loadedIds = new Set(sessions.map(s => s.claudeSessionId));
|
|
299
|
+
const persisted = loadPersistedSessions().filter(p => !loadedIds.has(p.claudeSessionId));
|
|
300
|
+
if (persisted.length > 0) {
|
|
301
|
+
items.push({ label: ' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', action: 'sep' });
|
|
302
|
+
items.push({ label: ' {#888888-fg}Previous Sessions{/}', action: 'sep' });
|
|
303
|
+
persisted.forEach(p => {
|
|
304
|
+
items.push({
|
|
305
|
+
label: ` {yellow-fg}\u21bb{/} [${p.id}] ${p.name}`,
|
|
306
|
+
action: `session-resume-${p.claudeSessionId}`,
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
265
311
|
if (sessions.length > 0) {
|
|
266
312
|
items.push({ label: ' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', action: 'sep' });
|
|
313
|
+
items.push({ label: ' {#888888-fg}Active Sessions{/}', action: 'sep' });
|
|
267
314
|
sessions.forEach((s, i) => {
|
|
268
315
|
const status = s.alive ? '{green-fg}\u25cf{/}' : '{red-fg}\u25cb{/}';
|
|
269
316
|
const active = (i === activeSessionIdx) ? '{#4a9090-fg}\u25b8{/} ' : ' ';
|
|
@@ -275,6 +322,7 @@ function refreshSessionMenu() {
|
|
|
275
322
|
items.push({ label: ' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', action: 'sep' });
|
|
276
323
|
items.push({ label: ' Kill Session', action: 'session-kill' });
|
|
277
324
|
}
|
|
325
|
+
|
|
278
326
|
mod.items = items;
|
|
279
327
|
// If Sessions module is active, refresh the visible menu
|
|
280
328
|
if (activeModuleIdx === 3) {
|
|
@@ -284,18 +332,28 @@ function refreshSessionMenu() {
|
|
|
284
332
|
}
|
|
285
333
|
}
|
|
286
334
|
|
|
287
|
-
function createSession(name, cwd) {
|
|
335
|
+
function createSession(name, cwd, resumeSessionId) {
|
|
288
336
|
if (!XTerm) {
|
|
289
337
|
toast('Sessions require blessed-xterm (native rebuild needed). Run: npm rebuild', true);
|
|
290
338
|
return null;
|
|
291
339
|
}
|
|
340
|
+
// Guard: fall back to process.cwd() if provided cwd no longer exists (e.g., resumed session)
|
|
341
|
+
let effectiveCwd = cwd || process.cwd();
|
|
342
|
+
if (!fs.existsSync(effectiveCwd)) {
|
|
343
|
+
logEvent('warn', `Session cwd "${effectiveCwd}" no longer exists, falling back to ${process.cwd()}`);
|
|
344
|
+
effectiveCwd = process.cwd();
|
|
345
|
+
}
|
|
292
346
|
const id = ++sessionIdCounter;
|
|
347
|
+
const claudeSessionId = resumeSessionId || crypto.randomUUID();
|
|
348
|
+
const args = resumeSessionId
|
|
349
|
+
? ['--resume', resumeSessionId]
|
|
350
|
+
: ['--session-id', claudeSessionId];
|
|
293
351
|
const term = new XTerm({
|
|
294
352
|
screen,
|
|
295
353
|
parent: screen,
|
|
296
354
|
shell: 'claude',
|
|
297
|
-
args
|
|
298
|
-
cwd:
|
|
355
|
+
args,
|
|
356
|
+
cwd: effectiveCwd,
|
|
299
357
|
cursorType: 'block',
|
|
300
358
|
scrollback: 1000,
|
|
301
359
|
top: 3, left: 35, right: 0, bottom: 2,
|
|
@@ -311,8 +369,9 @@ function createSession(name, cwd) {
|
|
|
311
369
|
});
|
|
312
370
|
term.hide();
|
|
313
371
|
|
|
314
|
-
const session = { id, name, cwd:
|
|
372
|
+
const session = { id, name, cwd: effectiveCwd, claudeSessionId, term, alive: true };
|
|
315
373
|
sessions.push(session);
|
|
374
|
+
savePersistedSessions();
|
|
316
375
|
|
|
317
376
|
term.on('exit', () => {
|
|
318
377
|
session.alive = false;
|
|
@@ -358,6 +417,7 @@ function killSession(idx) {
|
|
|
358
417
|
const session = sessions[idx];
|
|
359
418
|
try { session.term.terminate(); } catch (_) {}
|
|
360
419
|
screen.remove(session.term);
|
|
420
|
+
removePersistedSession(session.claudeSessionId);
|
|
361
421
|
sessions.splice(idx, 1);
|
|
362
422
|
// Adjust activeSessionIdx
|
|
363
423
|
if (activeSessionIdx === idx) {
|
|
@@ -376,13 +436,31 @@ async function newSessionFlow() {
|
|
|
376
436
|
}
|
|
377
437
|
|
|
378
438
|
async function killSessionFlow() {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
439
|
+
const items = [];
|
|
440
|
+
sessions.forEach((s, i) => {
|
|
441
|
+
items.push({
|
|
442
|
+
label: `[${s.id}] ${s.name} (${s.alive ? 'alive' : 'dead'})`,
|
|
443
|
+
value: { type: 'active', idx: i },
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
// Also offer to remove persisted (inactive) sessions
|
|
447
|
+
const loadedIds = new Set(sessions.map(s => s.claudeSessionId));
|
|
448
|
+
const persisted = loadPersistedSessions().filter(p => !loadedIds.has(p.claudeSessionId));
|
|
449
|
+
persisted.forEach(p => {
|
|
450
|
+
items.push({
|
|
451
|
+
label: `[${p.id}] ${p.name} (saved)`,
|
|
452
|
+
value: { type: 'persisted', claudeSessionId: p.claudeSessionId },
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
if (items.length === 0) { toast('No sessions to kill'); return; }
|
|
384
456
|
const choice = await promptList({ title: 'Kill Session', items });
|
|
385
|
-
|
|
457
|
+
if (choice.value.type === 'active') {
|
|
458
|
+
killSession(choice.value.idx);
|
|
459
|
+
} else {
|
|
460
|
+
removePersistedSession(choice.value.claudeSessionId);
|
|
461
|
+
refreshSessionMenu();
|
|
462
|
+
toast('Saved session removed');
|
|
463
|
+
}
|
|
386
464
|
}
|
|
387
465
|
|
|
388
466
|
// ─── Providers.json helpers ───────────────────────────────────────────────────
|
|
@@ -397,11 +475,11 @@ function writeProvidersJson(data) {
|
|
|
397
475
|
|
|
398
476
|
// ─── Update policy helper ────────────────────────────────────────────────────
|
|
399
477
|
function writeUpdatePolicy(slotName, policy) {
|
|
400
|
-
const
|
|
401
|
-
if (!
|
|
402
|
-
if (!
|
|
403
|
-
|
|
404
|
-
|
|
478
|
+
const nfCfg = readNfJson();
|
|
479
|
+
if (!nf.agent_config) nf.agent_config = {};
|
|
480
|
+
if (!nf.agent_config[slotName]) nf.agent_config[slotName] = {};
|
|
481
|
+
nf.agent_config[slotName].update_policy = policy;
|
|
482
|
+
writeNfJson(nfCfg);
|
|
405
483
|
}
|
|
406
484
|
|
|
407
485
|
// ─── Secrets loader (cached — keychain prompted once per process) ─────────────
|
|
@@ -409,7 +487,7 @@ let _secretsCache = undefined;
|
|
|
409
487
|
function loadSecrets() {
|
|
410
488
|
if (_secretsCache !== undefined) return _secretsCache;
|
|
411
489
|
const candidates = [
|
|
412
|
-
path.join(os.homedir(), '.claude', '
|
|
490
|
+
path.join(os.homedir(), '.claude', 'nf-bin', 'secrets.cjs'),
|
|
413
491
|
path.join(__dirname, 'secrets.cjs'),
|
|
414
492
|
];
|
|
415
493
|
for (const p of candidates) {
|
|
@@ -521,7 +599,7 @@ function agentRows() {
|
|
|
521
599
|
const pp = require('./planning-paths.cjs');
|
|
522
600
|
failPath = pp.resolveWithFallback(process.cwd(), 'quorum-failures');
|
|
523
601
|
} catch (_) {
|
|
524
|
-
failPath = path.join(os.homedir(), '.claude', '
|
|
602
|
+
failPath = path.join(os.homedir(), '.claude', 'nf', 'quorum-failures.json');
|
|
525
603
|
}
|
|
526
604
|
if (fs.existsSync(failPath)) {
|
|
527
605
|
const failures = JSON.parse(fs.readFileSync(failPath, 'utf8'));
|
|
@@ -554,15 +632,15 @@ function buildHeaderInfo() {
|
|
|
554
632
|
let quorumN = '—';
|
|
555
633
|
let failMode = '—';
|
|
556
634
|
try {
|
|
557
|
-
const
|
|
558
|
-
const defN =
|
|
559
|
-
const byProf =
|
|
635
|
+
const nfCfg = readNfJson();
|
|
636
|
+
const defN = nf.quorum?.maxSize;
|
|
637
|
+
const byProf = nf.quorum?.maxSizeByProfile || {};
|
|
560
638
|
const effN = byProf[profile] ?? defN;
|
|
561
639
|
if (effN != null) quorumN = String(effN) + (byProf[profile] != null ? '*' : '');
|
|
562
|
-
if (
|
|
640
|
+
if (nf.fail_mode) failMode = nf.fail_mode;
|
|
563
641
|
} catch (_) {}
|
|
564
642
|
|
|
565
|
-
// Key agent tiers — from
|
|
643
|
+
// Key agent tiers — from core/references/model-profiles.md
|
|
566
644
|
const TIERS = {
|
|
567
645
|
planner: { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
568
646
|
executor: { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
@@ -674,19 +752,19 @@ function refreshStatusBar(extra) {
|
|
|
674
752
|
// ─── Settings content (rendered in contentBox when Settings action selected) ──
|
|
675
753
|
function buildSettingsPaneContent() {
|
|
676
754
|
const cfg = readProjectConfig();
|
|
677
|
-
const
|
|
755
|
+
const nfCfg = readNfJson();
|
|
678
756
|
const profile = cfg.model_profile || 'balanced';
|
|
679
757
|
const ov = cfg.model_overrides || {};
|
|
680
|
-
const defN =
|
|
681
|
-
const byProf =
|
|
758
|
+
const defN = nf.quorum?.maxSize ?? 3;
|
|
759
|
+
const byProf = nf.quorum?.maxSizeByProfile || {};
|
|
682
760
|
const effN = byProf[profile] ?? defN;
|
|
683
761
|
const nStr = String(effN) + (byProf[profile] != null ? '*' : '');
|
|
684
|
-
const failStr =
|
|
762
|
+
const failStr = nf.fail_mode || '—';
|
|
685
763
|
|
|
686
764
|
const D = '{#777777-fg}', V = '{#aaaaaa-fg}', A = '{#4a9090-fg}', Z = '{/}';
|
|
687
765
|
const mTag = k => ov[k] ? `${A}${ov[k]}${Z}{#888888-fg}*${Z}` : `${V}${AGENT_TIERS[k]?.[profile] || '—'}${Z}`;
|
|
688
766
|
|
|
689
|
-
const agents = ['
|
|
767
|
+
const agents = ['nf-planner', 'nf-executor', 'nf-phase-researcher', 'nf-verifier', 'nf-codebase-mapper'];
|
|
690
768
|
const labels = ['Planner', 'Executor', 'Researcher', 'Verifier', 'Mapper'];
|
|
691
769
|
|
|
692
770
|
const lines = [
|
|
@@ -865,7 +943,7 @@ function promptCheckbox(opts) {
|
|
|
865
943
|
// Writes a temp shell script and opens it in a new Terminal.app window via osascript.
|
|
866
944
|
// Returns true if the terminal was opened successfully.
|
|
867
945
|
function spawnExternalTerminal(loginCmd) {
|
|
868
|
-
const tmpScript = path.join(os.tmpdir(), '
|
|
946
|
+
const tmpScript = path.join(os.tmpdir(), 'nf-auth-' + Date.now() + '.sh');
|
|
869
947
|
try {
|
|
870
948
|
fs.writeFileSync(tmpScript, [
|
|
871
949
|
'#!/bin/sh',
|
|
@@ -1073,7 +1151,7 @@ async function addAgentFlow() {
|
|
|
1073
1151
|
|
|
1074
1152
|
const secrets = loadSecrets();
|
|
1075
1153
|
if (apiKey && secrets) {
|
|
1076
|
-
await secrets.set('
|
|
1154
|
+
await secrets.set('nforma', deriveKeytarAccount(slotName), apiKey);
|
|
1077
1155
|
} else if (apiKey) {
|
|
1078
1156
|
env.ANTHROPIC_API_KEY = apiKey;
|
|
1079
1157
|
}
|
|
@@ -1104,20 +1182,20 @@ async function cloneSlotFlow() {
|
|
|
1104
1182
|
data.mcpServers = { ...servers, [newName]: cloned };
|
|
1105
1183
|
writeClaudeJson(data);
|
|
1106
1184
|
|
|
1107
|
-
// Copy
|
|
1185
|
+
// Copy nf.json agent_config metadata from source to cloned slot
|
|
1108
1186
|
try {
|
|
1109
|
-
const
|
|
1110
|
-
const sourceConfig = (
|
|
1187
|
+
const nfCfg = readNfJson();
|
|
1188
|
+
const sourceConfig = (nf.agent_config || {})[source.value];
|
|
1111
1189
|
if (sourceConfig) {
|
|
1112
|
-
if (!
|
|
1113
|
-
|
|
1190
|
+
if (!nf.agent_config) nf.agent_config = {};
|
|
1191
|
+
nf.agent_config[newName] = JSON.parse(JSON.stringify(sourceConfig));
|
|
1114
1192
|
// Clear key_status from clone (needs fresh probe)
|
|
1115
|
-
if (
|
|
1116
|
-
delete
|
|
1193
|
+
if (nf.agent_config[newName].key_status) {
|
|
1194
|
+
delete nf.agent_config[newName].key_status;
|
|
1117
1195
|
}
|
|
1118
|
-
|
|
1196
|
+
writeNfJson(nfCfg);
|
|
1119
1197
|
}
|
|
1120
|
-
} catch (_) { /*
|
|
1198
|
+
} catch (_) { /* nf.json might not exist yet -- non-fatal */ }
|
|
1121
1199
|
|
|
1122
1200
|
toast(`✓ Cloned "${source.value}" → "${newName}"`);
|
|
1123
1201
|
renderList();
|
|
@@ -1193,9 +1271,9 @@ async function editAgentFlow() {
|
|
|
1193
1271
|
prompt: 'ANTHROPIC_API_KEY (blank = remove):', isPassword: true });
|
|
1194
1272
|
const secrets = loadSecrets();
|
|
1195
1273
|
const account = deriveKeytarAccount(slotName);
|
|
1196
|
-
if (val && secrets) { await secrets.set('
|
|
1274
|
+
if (val && secrets) { await secrets.set('nforma', account, val); delete env.ANTHROPIC_API_KEY; }
|
|
1197
1275
|
else if (val) { env.ANTHROPIC_API_KEY = val; }
|
|
1198
|
-
else if (secrets) { await secrets.delete('
|
|
1276
|
+
else if (secrets) { await secrets.delete('nforma', account); delete env.ANTHROPIC_API_KEY; }
|
|
1199
1277
|
else { delete env.ANTHROPIC_API_KEY; }
|
|
1200
1278
|
|
|
1201
1279
|
} else if (field.value === 'baseUrl') {
|
|
@@ -1478,7 +1556,7 @@ async function loginAgentFlow() {
|
|
|
1478
1556
|
// ─── Provider Keys ────────────────────────────────────────────────────────────
|
|
1479
1557
|
async function renderProviderKeys() {
|
|
1480
1558
|
const secrets = loadSecrets();
|
|
1481
|
-
if (!secrets) { setContent('Provider Keys', '{red-fg}secrets.cjs not found —
|
|
1559
|
+
if (!secrets) { setContent('Provider Keys', '{red-fg}secrets.cjs not found — nForma not installed.{/}'); return; }
|
|
1482
1560
|
const lines = ['{bold}Provider Keys (keytar){/bold}', '─'.repeat(40)];
|
|
1483
1561
|
for (const { key, label } of PROVIDER_KEY_NAMES) {
|
|
1484
1562
|
// hasKey() reads local JSON index — no keychain prompt
|
|
@@ -1492,7 +1570,7 @@ async function renderProviderKeys() {
|
|
|
1492
1570
|
async function providerKeysFlow() {
|
|
1493
1571
|
setContent('Provider Keys', '{gray-fg}Select an action…{/}');
|
|
1494
1572
|
const secrets = loadSecrets();
|
|
1495
|
-
if (!secrets) { toast('secrets.cjs not found —
|
|
1573
|
+
if (!secrets) { toast('secrets.cjs not found — nForma not installed', true); return; }
|
|
1496
1574
|
|
|
1497
1575
|
while (true) { // action loop: ESC → main menu
|
|
1498
1576
|
let action;
|
|
@@ -1518,7 +1596,7 @@ async function providerKeysFlow() {
|
|
|
1518
1596
|
|
|
1519
1597
|
try {
|
|
1520
1598
|
if (action.value === 'remove') {
|
|
1521
|
-
await secrets.delete('
|
|
1599
|
+
await secrets.delete('nforma', picked.value);
|
|
1522
1600
|
toast(`Removed ${picked.label}`);
|
|
1523
1601
|
await renderProviderKeys();
|
|
1524
1602
|
continue; // re-show key picker (remove more)
|
|
@@ -1526,7 +1604,7 @@ async function providerKeysFlow() {
|
|
|
1526
1604
|
|
|
1527
1605
|
const val = await promptInput({ title: `Set ${picked.label}`, prompt: `Value for ${picked.value}:`, isPassword: true });
|
|
1528
1606
|
if (!val) { toast('Empty value — key not stored', true); continue; }
|
|
1529
|
-
await secrets.set('
|
|
1607
|
+
await secrets.set('nforma', picked.value, val);
|
|
1530
1608
|
toast(`${picked.label} saved to keychain`);
|
|
1531
1609
|
await renderProviderKeys();
|
|
1532
1610
|
} catch (_) { continue; } // ESC during value input → re-show key picker
|
|
@@ -1537,7 +1615,7 @@ async function providerKeysFlow() {
|
|
|
1537
1615
|
// ─── Post-Rotation Validation (CRED-01: fire-and-forget, non-blocking) ───────
|
|
1538
1616
|
/**
|
|
1539
1617
|
* Fire-and-forget post-rotation validation.
|
|
1540
|
-
* Probes each rotated slot and persists key_status to
|
|
1618
|
+
* Probes each rotated slot and persists key_status to nf.json.
|
|
1541
1619
|
* Does NOT block the caller -- called with .catch(() => {}).
|
|
1542
1620
|
* Uses sequential for...of to avoid keychain concurrency (same pattern as rotation loop).
|
|
1543
1621
|
* Reuses probeAndPersistKey from manage-agents-core.cjs (DRY -- do not duplicate probe/classify/write logic).
|
|
@@ -1556,7 +1634,7 @@ async function validateRotatedKeys(rotatedSlots) {
|
|
|
1556
1634
|
if (secretsLib) {
|
|
1557
1635
|
try {
|
|
1558
1636
|
const account = deriveKeytarAccount(slotName);
|
|
1559
|
-
const k = await secretsLib.get('
|
|
1637
|
+
const k = await secretsLib.get('nforma', account);
|
|
1560
1638
|
if (k) apiKey = k;
|
|
1561
1639
|
} catch (_) {}
|
|
1562
1640
|
}
|
|
@@ -1604,7 +1682,7 @@ async function batchRotateFlow() {
|
|
|
1604
1682
|
|
|
1605
1683
|
const account = deriveKeytarAccount(picked.value);
|
|
1606
1684
|
if (secrets) {
|
|
1607
|
-
await secrets.set('
|
|
1685
|
+
await secrets.set('nforma', account, newKey);
|
|
1608
1686
|
} else {
|
|
1609
1687
|
if (!servers[picked.value].env) servers[picked.value].env = {};
|
|
1610
1688
|
servers[picked.value].env.ANTHROPIC_API_KEY = newKey;
|
|
@@ -1671,7 +1749,7 @@ function buildScoreboardLines(data, opts) {
|
|
|
1671
1749
|
const lines = [];
|
|
1672
1750
|
lines.push('{bold} Quorum Scoreboard{/bold}');
|
|
1673
1751
|
lines.push(' {gray-fg}No agents configured in providers.json.{/}');
|
|
1674
|
-
lines.push(' {gray-fg}Run /
|
|
1752
|
+
lines.push(' {gray-fg}Run /nf:mcp-setup to add agents.{/}');
|
|
1675
1753
|
lines.push('');
|
|
1676
1754
|
return lines;
|
|
1677
1755
|
}
|
|
@@ -1973,18 +2051,18 @@ async function updateAgentsFlow() {
|
|
|
1973
2051
|
|
|
1974
2052
|
// ─── Settings helpers ─────────────────────────────────────────────────────────
|
|
1975
2053
|
const AGENT_TIERS = {
|
|
1976
|
-
'
|
|
1977
|
-
'
|
|
1978
|
-
'
|
|
1979
|
-
'
|
|
1980
|
-
'
|
|
2054
|
+
'nf-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
|
|
2055
|
+
'nf-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
|
|
2056
|
+
'nf-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
|
|
2057
|
+
'nf-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
|
|
2058
|
+
'nf-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
|
|
1981
2059
|
};
|
|
1982
2060
|
const AGENT_LABELS = {
|
|
1983
|
-
'
|
|
1984
|
-
'
|
|
1985
|
-
'
|
|
1986
|
-
'
|
|
1987
|
-
'
|
|
2061
|
+
'nf-planner': 'Planner',
|
|
2062
|
+
'nf-executor': 'Executor',
|
|
2063
|
+
'nf-phase-researcher': 'Researcher',
|
|
2064
|
+
'nf-verifier': 'Verifier',
|
|
2065
|
+
'nf-codebase-mapper': 'Mapper',
|
|
1988
2066
|
};
|
|
1989
2067
|
|
|
1990
2068
|
function readProjectConfig() {
|
|
@@ -2001,10 +2079,10 @@ function writeProjectConfig(cfg) {
|
|
|
2001
2079
|
async function settingsFlow() {
|
|
2002
2080
|
while (true) {
|
|
2003
2081
|
const cfg = readProjectConfig();
|
|
2004
|
-
const
|
|
2082
|
+
const nfCfg = readNfJson();
|
|
2005
2083
|
const profile = cfg.model_profile || 'balanced';
|
|
2006
|
-
const defN =
|
|
2007
|
-
const byProf =
|
|
2084
|
+
const defN = nf.quorum?.maxSize ?? 3;
|
|
2085
|
+
const byProf = nf.quorum?.maxSizeByProfile || {};
|
|
2008
2086
|
const effN = byProf[profile] ?? defN;
|
|
2009
2087
|
const nStr = String(effN) + (byProf[profile] != null ? '*' : '');
|
|
2010
2088
|
const ovCount = Object.keys(cfg.model_overrides || {}).length;
|
|
@@ -2014,7 +2092,7 @@ async function settingsFlow() {
|
|
|
2014
2092
|
picked = await promptList({ title: 'Settings', items: [
|
|
2015
2093
|
{ label: ` Profile ${profile}`, value: 'profile' },
|
|
2016
2094
|
{ label: ` Quorum n ${nStr} →`, value: 'n' },
|
|
2017
|
-
{ label: ` Fail mode ${
|
|
2095
|
+
{ label: ` Fail mode ${nf.fail_mode || '—'}`,value: 'fail' },
|
|
2018
2096
|
{ label: ` Model overrides ${ovCount ? `${ovCount} active` : 'none'} →`, value: 'overrides' },
|
|
2019
2097
|
]});
|
|
2020
2098
|
} catch (_) { return; }
|
|
@@ -2046,9 +2124,9 @@ async function settingsFlow() {
|
|
|
2046
2124
|
{ label: 'strict Block if any required slot fails', value: 'strict' },
|
|
2047
2125
|
]});
|
|
2048
2126
|
} catch (_) { continue; }
|
|
2049
|
-
const qg =
|
|
2127
|
+
const qg = readNfJson();
|
|
2050
2128
|
qg.fail_mode = choice.value;
|
|
2051
|
-
|
|
2129
|
+
writeNfJson(qg);
|
|
2052
2130
|
refreshSettingsPane();
|
|
2053
2131
|
toast(`✓ Fail mode → ${choice.value}`);
|
|
2054
2132
|
|
|
@@ -2109,9 +2187,9 @@ async function modelOverridesFlow() {
|
|
|
2109
2187
|
|
|
2110
2188
|
async function quorumNFlow() {
|
|
2111
2189
|
while (true) {
|
|
2112
|
-
const
|
|
2113
|
-
const defN =
|
|
2114
|
-
const byProf =
|
|
2190
|
+
const nfCfg = readNfJson();
|
|
2191
|
+
const defN = nf.quorum?.maxSize ?? 3;
|
|
2192
|
+
const byProf = nf.quorum?.maxSizeByProfile || {};
|
|
2115
2193
|
const fmt = p => byProf[p] != null ? String(byProf[p]) : `${defN} (default)`;
|
|
2116
2194
|
|
|
2117
2195
|
let picked;
|
|
@@ -2138,19 +2216,19 @@ async function quorumNFlow() {
|
|
|
2138
2216
|
const n = parseInt(val.trim(), 10);
|
|
2139
2217
|
if (isNaN(n) || n < 0) { toast('Invalid — enter a positive number or 0 to remove', true); continue; }
|
|
2140
2218
|
|
|
2141
|
-
if (!
|
|
2219
|
+
if (!nf.quorum) nf.quorum = {};
|
|
2142
2220
|
if (picked.value === 'default') {
|
|
2143
|
-
|
|
2221
|
+
nf.quorum.maxSize = n;
|
|
2144
2222
|
} else {
|
|
2145
|
-
if (!
|
|
2223
|
+
if (!nf.quorum.maxSizeByProfile) nf.quorum.maxSizeByProfile = {};
|
|
2146
2224
|
if (n === 0) {
|
|
2147
|
-
delete
|
|
2148
|
-
if (!Object.keys(
|
|
2225
|
+
delete nf.quorum.maxSizeByProfile[picked.value];
|
|
2226
|
+
if (!Object.keys(nf.quorum.maxSizeByProfile).length) delete nf.quorum.maxSizeByProfile;
|
|
2149
2227
|
} else {
|
|
2150
|
-
|
|
2228
|
+
nf.quorum.maxSizeByProfile[picked.value] = n;
|
|
2151
2229
|
}
|
|
2152
2230
|
}
|
|
2153
|
-
|
|
2231
|
+
writeNfJson(nfCfg);
|
|
2154
2232
|
renderHeader();
|
|
2155
2233
|
refreshSettingsPane();
|
|
2156
2234
|
toast(`✓ Quorum n (${picked.value}) → ${n === 0 ? 'reset to default' : n}`);
|
|
@@ -2209,8 +2287,8 @@ async function updatePolicyFlow() {
|
|
|
2209
2287
|
const slots = Object.keys(servers);
|
|
2210
2288
|
if (!slots.length) { toast('No slots configured', true); return; }
|
|
2211
2289
|
|
|
2212
|
-
const
|
|
2213
|
-
const agentConfig =
|
|
2290
|
+
const nfCfg = readNfJson();
|
|
2291
|
+
const agentConfig = nf.agent_config || {};
|
|
2214
2292
|
|
|
2215
2293
|
const target = await promptList({ title: 'Update Policy — Pick slot',
|
|
2216
2294
|
items: slots.map(s => ({
|
|
@@ -2276,7 +2354,7 @@ async function importFlow() {
|
|
|
2276
2354
|
prompt: `API key for ${slotName} / ${envKey} (blank = skip):`, isPassword: true });
|
|
2277
2355
|
} catch (_) { delete cfg.env[envKey]; continue; } // ESC → skip key, move to next
|
|
2278
2356
|
if (val) {
|
|
2279
|
-
if (secrets) { await secrets.set('
|
|
2357
|
+
if (secrets) { await secrets.set('nforma', deriveKeytarAccount(slotName), val); delete cfg.env[envKey]; }
|
|
2280
2358
|
else { cfg.env[envKey] = val; }
|
|
2281
2359
|
} else {
|
|
2282
2360
|
delete cfg.env[envKey];
|
|
@@ -2335,6 +2413,13 @@ async function dispatch(action) {
|
|
|
2335
2413
|
else if (action === 'req-gaps') reqCoverageGapsFlow();
|
|
2336
2414
|
else if (action === 'session-new') await newSessionFlow();
|
|
2337
2415
|
else if (action === 'session-kill') await killSessionFlow();
|
|
2416
|
+
else if (action.startsWith('session-resume-')) {
|
|
2417
|
+
const csid = action.replace('session-resume-', '');
|
|
2418
|
+
const persisted = loadPersistedSessions().find(p => p.claudeSessionId === csid);
|
|
2419
|
+
if (persisted) {
|
|
2420
|
+
createSession(persisted.name, persisted.cwd, persisted.claudeSessionId);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2338
2423
|
else if (action.startsWith('session-connect-')) {
|
|
2339
2424
|
connectSession(parseInt(action.replace('session-connect-', ''), 10));
|
|
2340
2425
|
}
|
|
@@ -2568,7 +2653,7 @@ function reqCoverageGapsFlow() {
|
|
|
2568
2653
|
lines.push('');
|
|
2569
2654
|
|
|
2570
2655
|
// Run for all known specs
|
|
2571
|
-
const specs = ['
|
|
2656
|
+
const specs = ['NFQuorum', 'NFStopHook', 'NFCircuitBreaker'];
|
|
2572
2657
|
let totalGaps = 0;
|
|
2573
2658
|
|
|
2574
2659
|
for (const specName of specs) {
|
|
@@ -2698,6 +2783,12 @@ function applyUpdateBadge(outdatedCount) {
|
|
|
2698
2783
|
// ─── Start ────────────────────────────────────────────────────────────────────
|
|
2699
2784
|
if (require.main === module) {
|
|
2700
2785
|
renderHeader();
|
|
2786
|
+
// Restore persisted session counter so IDs don't collide
|
|
2787
|
+
const _persisted = loadPersistedSessions();
|
|
2788
|
+
if (_persisted.length > 0) {
|
|
2789
|
+
sessionIdCounter = Math.max(..._persisted.map(p => p.id));
|
|
2790
|
+
}
|
|
2791
|
+
refreshSessionMenu();
|
|
2701
2792
|
switchModule(0);
|
|
2702
2793
|
const warns = _logEntries.filter(e => e.level === 'warn' || e.level === 'error');
|
|
2703
2794
|
if (warns.length) {
|
|
@@ -2723,4 +2814,8 @@ module.exports._pure = {
|
|
|
2723
2814
|
MODULES,
|
|
2724
2815
|
logEvent,
|
|
2725
2816
|
_logEntries,
|
|
2817
|
+
loadPersistedSessions,
|
|
2818
|
+
savePersistedSessions,
|
|
2819
|
+
removePersistedSession,
|
|
2820
|
+
SESSIONS_FILE,
|
|
2726
2821
|
};
|