@nforma.ai/nforma 0.2.1 → 0.29.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.
Files changed (193) hide show
  1. package/README.md +2 -2
  2. package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
  3. package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
  4. package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
  5. package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
  6. package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
  7. package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
  8. package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
  9. package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
  10. package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
  11. package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
  12. package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
  13. package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
  14. package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
  15. package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
  16. package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
  17. package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
  18. package/bin/accept-debug-invariant.cjs +2 -2
  19. package/bin/account-manager.cjs +10 -10
  20. package/bin/aggregate-requirements.cjs +1 -1
  21. package/bin/analyze-assumptions.cjs +3 -3
  22. package/bin/analyze-state-space.cjs +14 -14
  23. package/bin/assumption-register.cjs +146 -0
  24. package/bin/attribute-trace-divergence.cjs +1 -1
  25. package/bin/auth-drivers/gh-cli.cjs +1 -1
  26. package/bin/auth-drivers/pool.cjs +1 -1
  27. package/bin/autoClosePtoF.cjs +3 -3
  28. package/bin/budget-tracker.cjs +77 -0
  29. package/bin/build-layer-manifest.cjs +153 -0
  30. package/bin/call-quorum-slot.cjs +3 -3
  31. package/bin/ccr-secure-config.cjs +5 -5
  32. package/bin/check-bundled-sdks.cjs +1 -1
  33. package/bin/check-mcp-health.cjs +1 -1
  34. package/bin/check-provider-health.cjs +6 -6
  35. package/bin/check-spec-sync.cjs +26 -26
  36. package/bin/check-trace-schema-drift.cjs +5 -5
  37. package/bin/conformance-schema.cjs +2 -2
  38. package/bin/cross-layer-dashboard.cjs +297 -0
  39. package/bin/design-impact.cjs +377 -0
  40. package/bin/detect-coverage-gaps.cjs +7 -7
  41. package/bin/failure-mode-catalog.cjs +227 -0
  42. package/bin/failure-taxonomy.cjs +177 -0
  43. package/bin/formal-scope-scan.cjs +179 -0
  44. package/bin/gate-a-grounding.cjs +334 -0
  45. package/bin/gate-b-abstraction.cjs +243 -0
  46. package/bin/gate-c-validation.cjs +166 -0
  47. package/bin/generate-formal-specs.cjs +17 -17
  48. package/bin/generate-petri-net.cjs +3 -3
  49. package/bin/generate-tla-cfg.cjs +5 -5
  50. package/bin/git-heatmap.cjs +571 -0
  51. package/bin/harness-diagnostic.cjs +326 -0
  52. package/bin/hazard-model.cjs +261 -0
  53. package/bin/install-formal-tools.cjs +1 -1
  54. package/bin/install.js +184 -139
  55. package/bin/instrumentation-map.cjs +178 -0
  56. package/bin/invariant-catalog.cjs +437 -0
  57. package/bin/issue-classifier.cjs +2 -2
  58. package/bin/load-baseline-requirements.cjs +4 -4
  59. package/bin/manage-agents-core.cjs +32 -32
  60. package/bin/migrate-to-slots.cjs +39 -39
  61. package/bin/mismatch-register.cjs +217 -0
  62. package/bin/nForma.cjs +176 -81
  63. package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
  64. package/bin/observe-config.cjs +8 -0
  65. package/bin/observe-debt-writer.cjs +1 -1
  66. package/bin/observe-handler-deps.cjs +356 -0
  67. package/bin/observe-handler-grafana.cjs +2 -17
  68. package/bin/observe-handler-internal.cjs +5 -5
  69. package/bin/observe-handler-logstash.cjs +2 -17
  70. package/bin/observe-handler-prometheus.cjs +2 -17
  71. package/bin/observe-handler-upstream.cjs +251 -0
  72. package/bin/observe-handlers.cjs +12 -33
  73. package/bin/observe-render.cjs +68 -22
  74. package/bin/observe-utils.cjs +37 -0
  75. package/bin/observed-fsm.cjs +324 -0
  76. package/bin/planning-paths.cjs +6 -0
  77. package/bin/polyrepo.cjs +1 -1
  78. package/bin/probe-quorum-slots.cjs +1 -1
  79. package/bin/promote-gate-maturity.cjs +274 -0
  80. package/bin/promote-model.cjs +1 -1
  81. package/bin/propose-debug-invariants.cjs +1 -1
  82. package/bin/quorum-cache.cjs +144 -0
  83. package/bin/quorum-consensus-gate.cjs +1 -1
  84. package/bin/quorum-preflight.cjs +89 -0
  85. package/bin/quorum-slot-dispatch.cjs +6 -6
  86. package/bin/requirements-core.cjs +1 -1
  87. package/bin/review-mcp-logs.cjs +1 -1
  88. package/bin/risk-heatmap.cjs +151 -0
  89. package/bin/run-account-manager-tlc.cjs +4 -4
  90. package/bin/run-account-pool-alloy.cjs +2 -2
  91. package/bin/run-alloy.cjs +2 -2
  92. package/bin/run-audit-alloy.cjs +2 -2
  93. package/bin/run-breaker-tlc.cjs +3 -3
  94. package/bin/run-formal-check.cjs +9 -9
  95. package/bin/run-formal-verify.cjs +30 -9
  96. package/bin/run-installer-alloy.cjs +2 -2
  97. package/bin/run-oscillation-tlc.cjs +4 -4
  98. package/bin/run-phase-tlc.cjs +1 -1
  99. package/bin/run-protocol-tlc.cjs +4 -4
  100. package/bin/run-quorum-composition-alloy.cjs +2 -2
  101. package/bin/run-sensitivity-sweep.cjs +2 -2
  102. package/bin/run-stop-hook-tlc.cjs +3 -3
  103. package/bin/run-tlc.cjs +21 -21
  104. package/bin/run-transcript-alloy.cjs +2 -2
  105. package/bin/secrets.cjs +5 -5
  106. package/bin/security-sweep.cjs +238 -0
  107. package/bin/sensitivity-report.cjs +3 -3
  108. package/bin/set-secret.cjs +5 -5
  109. package/bin/setup-telemetry-cron.sh +3 -3
  110. package/bin/stall-detector.cjs +126 -0
  111. package/bin/state-candidates.cjs +206 -0
  112. package/bin/sync-baseline-requirements.cjs +1 -1
  113. package/bin/telemetry-collector.cjs +1 -1
  114. package/bin/test-changed.cjs +111 -0
  115. package/bin/test-recipe-gen.cjs +250 -0
  116. package/bin/trace-corpus-stats.cjs +211 -0
  117. package/bin/unified-mcp-server.mjs +3 -3
  118. package/bin/update-scoreboard.cjs +1 -1
  119. package/bin/validate-memory.cjs +2 -2
  120. package/bin/validate-traces.cjs +10 -10
  121. package/bin/verify-quorum-health.cjs +66 -5
  122. package/bin/xstate-to-tla.cjs +4 -4
  123. package/bin/xstate-trace-walker.cjs +3 -3
  124. package/commands/{qgsd → nf}/add-phase.md +3 -3
  125. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  126. package/commands/{qgsd → nf}/add-todo.md +3 -3
  127. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  128. package/commands/{qgsd → nf}/check-todos.md +3 -3
  129. package/commands/{qgsd → nf}/cleanup.md +3 -3
  130. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  131. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  132. package/commands/{qgsd → nf}/debug.md +9 -9
  133. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  134. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  135. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  136. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  137. package/commands/{qgsd → nf}/health.md +3 -3
  138. package/commands/{qgsd → nf}/help.md +3 -3
  139. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  140. package/commands/nf/join-discord.md +18 -0
  141. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  142. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  143. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  145. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  146. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  147. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  148. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  149. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  150. package/commands/{qgsd → nf}/new-project.md +8 -8
  151. package/commands/{qgsd → nf}/observe.md +49 -16
  152. package/commands/{qgsd → nf}/pause-work.md +3 -3
  153. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  154. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  155. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  156. package/commands/{qgsd → nf}/progress.md +3 -3
  157. package/commands/{qgsd → nf}/queue.md +2 -2
  158. package/commands/{qgsd → nf}/quick.md +8 -8
  159. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  160. package/commands/{qgsd → nf}/quorum.md +36 -86
  161. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  162. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  163. package/commands/{qgsd → nf}/research-phase.md +12 -12
  164. package/commands/{qgsd → nf}/resume-work.md +3 -3
  165. package/commands/nf/review-requirements.md +31 -0
  166. package/commands/{qgsd → nf}/set-profile.md +3 -3
  167. package/commands/{qgsd → nf}/settings.md +6 -6
  168. package/commands/{qgsd → nf}/solve.md +35 -35
  169. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  170. package/commands/{qgsd → nf}/triage.md +10 -10
  171. package/commands/{qgsd → nf}/update.md +3 -3
  172. package/commands/{qgsd → nf}/verify-work.md +5 -5
  173. package/hooks/dist/config-loader.js +188 -32
  174. package/hooks/dist/conformance-schema.cjs +2 -2
  175. package/hooks/dist/gsd-context-monitor.js +118 -13
  176. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  177. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  180. package/hooks/dist/nf-session-start.js +185 -0
  181. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  182. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  183. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  184. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  185. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  186. package/hooks/dist/unified-mcp-server.mjs +2 -2
  187. package/package.json +6 -4
  188. package/scripts/build-hooks.js +13 -6
  189. package/scripts/secret-audit.sh +1 -1
  190. package/scripts/verify-hooks-sync.cjs +90 -0
  191. package/templates/{qgsd.json → nf.json} +4 -4
  192. package/commands/qgsd/join-discord.md +0 -18
  193. 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
- readQgsdJson, writeQgsdJson,
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: cwd || process.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: cwd || process.cwd(), term, alive: true };
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
- if (sessions.length === 0) { toast('No sessions to kill'); return; }
380
- const items = sessions.map((s, i) => ({
381
- label: `[${s.id}] ${s.name} (${s.alive ? 'alive' : 'dead'})`,
382
- value: i,
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
- killSession(choice.value);
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 qgsd = readQgsdJson();
401
- if (!qgsd.agent_config) qgsd.agent_config = {};
402
- if (!qgsd.agent_config[slotName]) qgsd.agent_config[slotName] = {};
403
- qgsd.agent_config[slotName].update_policy = policy;
404
- writeQgsdJson(qgsd);
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', 'qgsd-bin', 'secrets.cjs'),
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', 'qgsd', 'quorum-failures.json');
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 qgsd = readQgsdJson();
558
- const defN = qgsd.quorum?.maxSize;
559
- const byProf = qgsd.quorum?.maxSizeByProfile || {};
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 (qgsd.fail_mode) failMode = qgsd.fail_mode;
640
+ if (nf.fail_mode) failMode = nf.fail_mode;
563
641
  } catch (_) {}
564
642
 
565
- // Key agent tiers — from qgsd-core/references/model-profiles.md
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 qgsd = readQgsdJson();
755
+ const nfCfg = readNfJson();
678
756
  const profile = cfg.model_profile || 'balanced';
679
757
  const ov = cfg.model_overrides || {};
680
- const defN = qgsd.quorum?.maxSize ?? 3;
681
- const byProf = qgsd.quorum?.maxSizeByProfile || {};
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 = qgsd.fail_mode || '—';
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 = ['qgsd-planner', 'qgsd-executor', 'qgsd-phase-researcher', 'qgsd-verifier', 'qgsd-codebase-mapper'];
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(), 'qgsd-auth-' + Date.now() + '.sh');
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('qgsd', deriveKeytarAccount(slotName), apiKey);
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 qgsd.json agent_config metadata from source to cloned slot
1185
+ // Copy nf.json agent_config metadata from source to cloned slot
1108
1186
  try {
1109
- const qgsd = readQgsdJson();
1110
- const sourceConfig = (qgsd.agent_config || {})[source.value];
1187
+ const nfCfg = readNfJson();
1188
+ const sourceConfig = (nf.agent_config || {})[source.value];
1111
1189
  if (sourceConfig) {
1112
- if (!qgsd.agent_config) qgsd.agent_config = {};
1113
- qgsd.agent_config[newName] = JSON.parse(JSON.stringify(sourceConfig));
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 (qgsd.agent_config[newName].key_status) {
1116
- delete qgsd.agent_config[newName].key_status;
1193
+ if (nf.agent_config[newName].key_status) {
1194
+ delete nf.agent_config[newName].key_status;
1117
1195
  }
1118
- writeQgsdJson(qgsd);
1196
+ writeNfJson(nfCfg);
1119
1197
  }
1120
- } catch (_) { /* qgsd.json might not exist yet -- non-fatal */ }
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('qgsd', account, val); delete env.ANTHROPIC_API_KEY; }
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('qgsd', account); delete env.ANTHROPIC_API_KEY; }
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 — QGSD not installed.{/}'); return; }
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 — QGSD not installed', true); return; }
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('qgsd', picked.value);
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('qgsd', picked.value, val);
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 qgsd.json.
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('qgsd', account);
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('qgsd', account, newKey);
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 /qgsd:mcp-setup to add agents.{/}');
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
- 'qgsd-planner': { quality: 'opus', balanced: 'opus', budget: 'sonnet' },
1977
- 'qgsd-executor': { quality: 'opus', balanced: 'sonnet', budget: 'sonnet' },
1978
- 'qgsd-phase-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
1979
- 'qgsd-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
1980
- 'qgsd-codebase-mapper': { quality: 'sonnet', balanced: 'haiku', budget: 'haiku' },
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
- 'qgsd-planner': 'Planner',
1984
- 'qgsd-executor': 'Executor',
1985
- 'qgsd-phase-researcher': 'Researcher',
1986
- 'qgsd-verifier': 'Verifier',
1987
- 'qgsd-codebase-mapper': 'Mapper',
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 qgsd = readQgsdJson();
2082
+ const nfCfg = readNfJson();
2005
2083
  const profile = cfg.model_profile || 'balanced';
2006
- const defN = qgsd.quorum?.maxSize ?? 3;
2007
- const byProf = qgsd.quorum?.maxSizeByProfile || {};
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 ${qgsd.fail_mode || '—'}`,value: 'fail' },
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 = readQgsdJson();
2127
+ const qg = readNfJson();
2050
2128
  qg.fail_mode = choice.value;
2051
- writeQgsdJson(qg);
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 qgsd = readQgsdJson();
2113
- const defN = qgsd.quorum?.maxSize ?? 3;
2114
- const byProf = qgsd.quorum?.maxSizeByProfile || {};
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 (!qgsd.quorum) qgsd.quorum = {};
2219
+ if (!nf.quorum) nf.quorum = {};
2142
2220
  if (picked.value === 'default') {
2143
- qgsd.quorum.maxSize = n;
2221
+ nf.quorum.maxSize = n;
2144
2222
  } else {
2145
- if (!qgsd.quorum.maxSizeByProfile) qgsd.quorum.maxSizeByProfile = {};
2223
+ if (!nf.quorum.maxSizeByProfile) nf.quorum.maxSizeByProfile = {};
2146
2224
  if (n === 0) {
2147
- delete qgsd.quorum.maxSizeByProfile[picked.value];
2148
- if (!Object.keys(qgsd.quorum.maxSizeByProfile).length) delete qgsd.quorum.maxSizeByProfile;
2225
+ delete nf.quorum.maxSizeByProfile[picked.value];
2226
+ if (!Object.keys(nf.quorum.maxSizeByProfile).length) delete nf.quorum.maxSizeByProfile;
2149
2227
  } else {
2150
- qgsd.quorum.maxSizeByProfile[picked.value] = n;
2228
+ nf.quorum.maxSizeByProfile[picked.value] = n;
2151
2229
  }
2152
2230
  }
2153
- writeQgsdJson(qgsd);
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 qgsd = readQgsdJson();
2213
- const agentConfig = qgsd.agent_config || {};
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('qgsd', deriveKeytarAccount(slotName), val); delete cfg.env[envKey]; }
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 = ['QGSDQuorum', 'QGSDStopHook', 'QGSDCircuitBreaker'];
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
  };