@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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-circuit-breaker.js
2
+ // hooks/nf-circuit-breaker.js
3
3
  // PreToolUse hook — oscillation detection, state persistence, and notification for circuit breaker.
4
4
  //
5
5
  // Reads JSON from stdin (Claude Code PreToolUse event payload), checks for oscillation
@@ -94,30 +94,38 @@ function getCommitDiff(gitRoot, olderHash, newerHash, files) {
94
94
 
95
95
  // Second-pass reversion check: given the hashes (newest-first) belonging to
96
96
  // run-groups for an oscillating file set key, and the files in that set,
97
- // determines whether the pattern is true oscillation or TDD progression.
97
+ // determines whether the pattern is true oscillation or TDD/workflow progression.
98
+ //
99
+ // Algorithm: sum net change (additions - deletions) across all consecutive pairs,
100
+ // AND track whether at least one pair has negative net change (content removal).
98
101
  //
99
- // Algorithm: sum net change (additions - deletions) across all consecutive pairs.
100
102
  // - Positive total net change → file grew overall → TDD progression (not oscillation).
101
- // - Zero or negative total net change file didn't grow true oscillation.
103
+ // - Zero or negative total net change WITH at least one pair showing net deletions
104
+ // → true oscillation (content was added then removed).
105
+ // - Zero or negative total net change with NO pair showing net deletions
106
+ // (all pairs are zero-net substitutions) → NOT oscillation. This is monotonic
107
+ // workflow progression (e.g., template → linter substitution → population).
102
108
  //
103
109
  // This correctly handles TDD patterns where a line like `module.exports` is modified
104
110
  // (1 deletion, 1 addition per commit) alongside net-new lines — the net change remains
105
111
  // positive because new functions are added each time.
106
112
  //
107
- // For true oscillation (same content toggled back and forth), each pair is symmetric
108
- // (same number added as removed) so the total net change is zero.
113
+ // For true oscillation (same content toggled back and forth), at least one pair
114
+ // will show a net-negative change (lines removed that were added in a prior pair).
109
115
  //
110
116
  // hashes: all commit hashes (newest-first) in the oscillating run-groups
111
117
  // files: file paths in the oscillating set
112
118
  // gitRoot: git repository root
113
119
  //
114
- // Returns true if real oscillation (net change <= 0), false if TDD progression (net change > 0).
120
+ // Returns true if real oscillation (net change <= 0 AND at least one negative pair).
121
+ // Returns false if all pairs are zero-net substitutions (monotonic workflow progression).
115
122
  // Returns true also if ALL pairs errored out (git unavailable → fall back to original behavior).
116
123
  function hasReversionInHashes(gitRoot, hashes, files) {
117
124
  // hashes are newest-first; consecutive pairs: (hashes[i], hashes[i-1]) where
118
125
  // hashes[i] is older (higher index = earlier in time), hashes[i-1] is newer.
119
126
  // We diff older → newer: git diff <hashes[i]> <hashes[i-1]>
120
127
  let totalNetChange = 0;
128
+ let hasNegativePair = false;
121
129
  let errorsOnly = true;
122
130
 
123
131
  for (let i = hashes.length - 1; i >= 1; i--) {
@@ -142,15 +150,18 @@ function hasReversionInHashes(gitRoot, hashes, files) {
142
150
  else if (line.startsWith('-')) deletions++;
143
151
  }
144
152
 
145
- totalNetChange += (additions - deletions);
153
+ const pairNet = additions - deletions;
154
+ totalNetChange += pairNet;
155
+ if (pairNet < 0) hasNegativePair = true;
146
156
  }
147
157
 
148
158
  // If all pairs errored out → fall back to original behavior (treat as oscillation)
149
159
  if (errorsOnly) return true;
150
160
 
151
161
  // Positive net change → file grew overall → TDD progression, not oscillation
152
- // Zero or negative net change file didn't grow → real oscillation
153
- return totalNetChange <= 0;
162
+ // Zero or negative net change WITH at least one negative pair → real oscillation
163
+ // Zero or negative net change with NO negative pair → monotonic substitution workflow, not oscillation
164
+ return totalNetChange <= 0 && hasNegativePair;
154
165
  }
155
166
 
156
167
  // Detects true oscillation: returns { detected: bool, fileSet: string[] }
@@ -301,7 +312,7 @@ function writeState(statePath, fileSet, snapshot) {
301
312
  };
302
313
  fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
303
314
  } catch (e) {
304
- process.stderr.write(`[qgsd] WARNING: Could not write circuit breaker state: ${e.message}\n`);
315
+ process.stderr.write(`[nf] WARNING: Could not write circuit breaker state: ${e.message}\n`);
305
316
  // Fail-open: do not block execution
306
317
  }
307
318
  }
@@ -329,7 +340,7 @@ function appendFalseNegative(statePath, fileSet) {
329
340
  });
330
341
  fs.writeFileSync(fnLogPath, JSON.stringify(existing, null, 2), 'utf8');
331
342
  } catch (e) {
332
- process.stderr.write(`[qgsd] WARNING: Could not write false-negative log: ${e.message}\n`);
343
+ process.stderr.write(`[nf] WARNING: Could not write false-negative log: ${e.message}\n`);
333
344
  // Fail-open: do not block execution
334
345
  }
335
346
  }
@@ -352,7 +363,7 @@ function writeOscillationLog(logPath, log) {
352
363
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
353
364
  fs.writeFileSync(logPath, JSON.stringify(log, null, 2), 'utf8');
354
365
  } catch (e) {
355
- process.stderr.write(`[qgsd] WARNING: Could not write oscillation log: ${e.message}\n`);
366
+ process.stderr.write(`[nf] WARNING: Could not write oscillation log: ${e.message}\n`);
356
367
  }
357
368
  }
358
369
 
@@ -387,7 +398,7 @@ function appendConformanceEvent(event) {
387
398
  const logPath = pp.resolve(process.cwd(), 'conformance-events');
388
399
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n', 'utf8');
389
400
  } catch (err) {
390
- process.stderr.write('[qgsd] conformance log write failed: ' + err.message + '\n');
401
+ process.stderr.write('[nf] conformance log write failed: ' + err.message + '\n');
391
402
  }
392
403
  }
393
404
 
@@ -416,12 +427,12 @@ function buildBlockReason(state) {
416
427
  lines.push('');
417
428
  }
418
429
  lines.push(
419
- 'Invoke Oscillation Resolution Mode per R5 in CLAUDE.md — see qgsd-core/workflows/oscillation-resolution-mode.md for the full procedure.',
430
+ 'Invoke Oscillation Resolution Mode per R5 in CLAUDE.md — see core/workflows/oscillation-resolution-mode.md for the full procedure.',
420
431
  '',
421
432
  'Read-only operations are still allowed (e.g. git log --oneline to review the commit history).',
422
433
  'You must manually commit a root-cause fix before write operations are unblocked.',
423
434
  '',
424
- "After committing the fix, run 'npx qgsd --reset-breaker' to clear the circuit breaker state.",
435
+ "After committing the fix, run 'npx nforma --reset-breaker' to clear the circuit breaker state.",
425
436
  );
426
437
  return lines.join('\n');
427
438
  }
@@ -453,11 +464,11 @@ function buildWarningNotice(state) {
453
464
  }
454
465
 
455
466
  lines.push(
456
- 'Invoke Oscillation Resolution Mode per R5 in CLAUDE.md — see qgsd-core/workflows/oscillation-resolution-mode.md for the full procedure.',
467
+ 'Invoke Oscillation Resolution Mode per R5 in CLAUDE.md — see core/workflows/oscillation-resolution-mode.md for the full procedure.',
457
468
  '',
458
- 'After committing the fix, run \'npx qgsd --reset-breaker\' to clear the circuit breaker state.',
459
- 'To temporarily disable the circuit breaker for deliberate iterative work, run \'npx qgsd --disable-breaker\'.',
460
- 'Re-enable with \'npx qgsd --enable-breaker\' when done.'
469
+ 'After committing the fix, run \'npx nforma --reset-breaker\' to clear the circuit breaker state.',
470
+ 'To temporarily disable the circuit breaker for deliberate iterative work, run \'npx nforma --disable-breaker\'.',
471
+ 'Re-enable with \'npx nforma --enable-breaker\' when done.'
461
472
  );
462
473
 
463
474
  return lines.join('\n');
@@ -568,10 +579,10 @@ req.end();
568
579
  // Clear state file so PreToolUse stops warning
569
580
  const statePath = path.join(gitRoot, '.claude', 'circuit-breaker-state.json');
570
581
  try { if (fs.existsSync(statePath)) fs.rmSync(statePath); } catch {}
571
- process.stderr.write(`[qgsd] INFO: Oscillation resolved by Haiku — circuit breaker cleared.\n`);
582
+ process.stderr.write(`[nf] INFO: Oscillation resolved by Haiku — circuit breaker cleared.\n`);
572
583
  }
573
584
  } catch (e) {
574
- process.stderr.write(`[qgsd] WARNING: PostToolUse Haiku check failed: ${e.message}\n`);
585
+ process.stderr.write(`[nf] WARNING: PostToolUse Haiku check failed: ${e.message}\n`);
575
586
  }
576
587
  process.exit(0);
577
588
  }
@@ -627,7 +638,7 @@ req.end();
627
638
  if (verdict === 'REFINEMENT') {
628
639
  // Haiku confirmed this is iterative refinement, not a bug loop — do not notify.
629
640
  // Log false-negative for auditability (stderr + persistent file).
630
- process.stderr.write(`[qgsd] INFO: circuit breaker false-negative — Haiku classified oscillation as REFINEMENT (files: ${result.fileSet.join(', ')}). Allowing tool call to proceed.\n`);
641
+ process.stderr.write(`[nf] INFO: circuit breaker false-negative — Haiku classified oscillation as REFINEMENT (files: ${result.fileSet.join(', ')}). Allowing tool call to proceed.\n`);
631
642
  appendFalseNegative(statePath, result.fileSet);
632
643
  process.exit(0);
633
644
  }
@@ -656,7 +667,7 @@ req.end();
656
667
  };
657
668
  writeOscillationLog(logPath, oscLog);
658
669
 
659
- // Write state so qgsd-prompt.js picks it up on next user message
670
+ // Write state so nf-prompt.js picks it up on next user message
660
671
  writeState(statePath, result.fileSet, fileSets);
661
672
 
662
673
  appendConformanceEvent({
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-precompact.js
3
- // PreCompact hook — injects QGSD session state as additionalContext before context compaction.
2
+ // hooks/nf-precompact.js
3
+ // PreCompact hook — injects nForma session state as additionalContext before context compaction.
4
4
  // Reads .planning/STATE.md "Current Position" section and any pending task files.
5
5
  // Output survives compaction and appears in the first message of the compacted context.
6
6
  // Fails open on all errors — never blocks compaction.
@@ -30,7 +30,7 @@ function extractCurrentPosition(stateContent) {
30
30
  return section.trim() || null;
31
31
  }
32
32
 
33
- // Read pending task files without consuming them (unlike qgsd-prompt.js's consumePendingTask).
33
+ // Read pending task files without consuming them (unlike nf-prompt.js's consumePendingTask).
34
34
  // Checks .claude/pending-task.txt and .claude/pending-task-*.txt files.
35
35
  // Returns an array of { filename, content } objects for each file found.
36
36
  function readPendingTasks(cwd) {
@@ -46,7 +46,7 @@ function readPendingTasks(cwd) {
46
46
  const content = fs.readFileSync(genericFile, 'utf8').trim();
47
47
  if (content) results.push({ filename: 'pending-task.txt', content });
48
48
  } catch (e) {
49
- process.stderr.write('[qgsd-precompact] Could not read ' + genericFile + ': ' + e.message + '\n');
49
+ process.stderr.write('[nf-precompact] Could not read ' + genericFile + ': ' + e.message + '\n');
50
50
  }
51
51
  }
52
52
 
@@ -60,12 +60,12 @@ function readPendingTasks(cwd) {
60
60
  const content = fs.readFileSync(filePath, 'utf8').trim();
61
61
  if (content) results.push({ filename: entry, content });
62
62
  } catch (e) {
63
- process.stderr.write('[qgsd-precompact] Could not read ' + filePath + ': ' + e.message + '\n');
63
+ process.stderr.write('[nf-precompact] Could not read ' + filePath + ': ' + e.message + '\n');
64
64
  }
65
65
  }
66
66
  }
67
67
  } catch (e) {
68
- process.stderr.write('[qgsd-precompact] Could not read .claude dir: ' + e.message + '\n');
68
+ process.stderr.write('[nf-precompact] Could not read .claude dir: ' + e.message + '\n');
69
69
  }
70
70
 
71
71
  return results;
@@ -85,14 +85,14 @@ process.stdin.on('end', () => {
85
85
 
86
86
  if (!fs.existsSync(statePath)) {
87
87
  // No STATE.md — minimal context
88
- additionalContext = 'QGSD session resumed after compaction. Run `cat .planning/STATE.md` for project state.';
88
+ additionalContext = 'nForma session resumed after compaction. Run `cat .planning/STATE.md` for project state.';
89
89
  } else {
90
90
  let stateContent;
91
91
  try {
92
92
  stateContent = fs.readFileSync(statePath, 'utf8');
93
93
  } catch (e) {
94
- process.stderr.write('[qgsd-precompact] Could not read STATE.md: ' + e.message + '\n');
95
- additionalContext = 'QGSD session resumed after compaction. Run `cat .planning/STATE.md` for project state.';
94
+ process.stderr.write('[nf-precompact] Could not read STATE.md: ' + e.message + '\n');
95
+ additionalContext = 'nForma session resumed after compaction. Run `cat .planning/STATE.md` for project state.';
96
96
  emitOutput(additionalContext);
97
97
  return;
98
98
  }
@@ -101,7 +101,7 @@ process.stdin.on('end', () => {
101
101
  const pendingTasks = readPendingTasks(cwd);
102
102
 
103
103
  const lines = [
104
- 'QGSD CONTINUATION CONTEXT (auto-injected at compaction)',
104
+ 'nForma CONTINUATION CONTEXT (auto-injected at compaction)',
105
105
  '',
106
106
  '## Current Position',
107
107
  currentPosition || '(Could not extract Current Position section — run `cat .planning/STATE.md` for full state.)',
@@ -113,13 +113,13 @@ process.stdin.on('end', () => {
113
113
  // Include the first pending task found (generic file takes priority)
114
114
  lines.push(pendingTasks[0].content);
115
115
  if (pendingTasks.length > 1) {
116
- process.stderr.write('[qgsd-precompact] Multiple pending task files found; injecting first: ' + pendingTasks[0].filename + '\n');
116
+ process.stderr.write('[nf-precompact] Multiple pending task files found; injecting first: ' + pendingTasks[0].filename + '\n');
117
117
  }
118
118
  }
119
119
 
120
120
  lines.push('');
121
121
  lines.push('## Resume Instructions');
122
- lines.push('You are mid-session on a QGSD project. The context above shows where you were.');
122
+ lines.push('You are mid-session on a nForma project. The context above shows where you were.');
123
123
  lines.push('- If a PLAN.md is in progress, continue executing from the current plan.');
124
124
  lines.push('- If a pending task is shown above, execute it next.');
125
125
  lines.push('- Run `cat .planning/STATE.md` to get full project state if needed.');
@@ -131,7 +131,7 @@ process.stdin.on('end', () => {
131
131
  emitOutput(additionalContext);
132
132
 
133
133
  } catch (e) {
134
- process.stderr.write('[qgsd-precompact] Fatal error: ' + e.message + '\n');
134
+ process.stderr.write('[nf-precompact] Fatal error: ' + e.message + '\n');
135
135
  process.exit(0); // Fail open — never block compaction
136
136
  }
137
137
  });
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-prompt.js
2
+ // hooks/nf-prompt.js
3
3
  // UserPromptSubmit hook — three responsibilities:
4
4
  //
5
5
  // 1. CIRCUIT BREAKER RECOVERY: If the circuit breaker is active, inject the
@@ -22,21 +22,21 @@ const fs = require('fs');
22
22
  const path = require('path');
23
23
  const os = require('os');
24
24
  const { spawnSync } = require('child_process');
25
- const { loadConfig, slotToToolCall } = require('./config-loader');
25
+ const { loadConfig, slotToToolCall, shouldRunHook } = require('./config-loader');
26
26
  const { schema_version } = require('./conformance-schema.cjs');
27
27
 
28
28
  const DEFAULT_QUORUM_INSTRUCTIONS_FALLBACK = `QUORUM REQUIRED (structural enforcement — Stop hook will verify)
29
29
 
30
- Run the full R3 quorum protocol inline (dispatch_pattern from commands/qgsd/quorum.md):
30
+ Run the full R3 quorum protocol inline (dispatch_pattern from commands/nf/quorum.md):
31
31
 
32
32
  1. State Claude's own position (vote) first — APPROVE or BLOCK with 1-2 sentence rationale
33
- 2. Run provider pre-flight: node ~/.claude/qgsd-bin/check-provider-health.cjs --json
34
- 3. Build $DISPATCH_LIST first (quorum.md Adaptive Fan-Out: read risk_level → compute FAN_OUT_COUNT → first FAN_OUT_COUNT-1 slots). Then dispatch $DISPATCH_LIST as sibling qgsd-quorum-slot-worker Tasks in one message turn — do NOT dispatch slots outside $DISPATCH_LIST:
35
- Task(subagent_type="qgsd-quorum-slot-worker", prompt="slot: <slot>\\nround: 1\\n...")
33
+ 2. Run provider pre-flight: node ~/.claude/nf-bin/check-provider-health.cjs --json
34
+ 3. Build $DISPATCH_LIST first (quorum.md Adaptive Fan-Out: read risk_level → compute FAN_OUT_COUNT → first FAN_OUT_COUNT-1 slots). Then dispatch $DISPATCH_LIST as sibling nf-quorum-slot-worker Tasks in one message turn — do NOT dispatch slots outside $DISPATCH_LIST:
35
+ Task(subagent_type="nf-quorum-slot-worker", prompt="slot: <slot>\\nround: 1\\n...")
36
36
  4. Synthesize results inline. Deliberate up to 10 rounds per R3.3 if no consensus.
37
- 5. Update scoreboard: node ~/.claude/qgsd-bin/update-scoreboard.cjs merge-wave ...
37
+ 5. Update scoreboard: node ~/.claude/nf-bin/update-scoreboard.cjs merge-wave ...
38
38
  6. [HEAL-01] After each deliberation round's merge-wave, check early escalation:
39
- node ~/.claude/qgsd-bin/quorum-consensus-gate.cjs --min-quorum=2 --remaining-rounds=R
39
+ node ~/.claude/nf-bin/quorum-consensus-gate.cjs --min-quorum=2 --remaining-rounds=R
40
40
  (R = maxDeliberation - currentRound). Exit code 1 = stop deliberating, proceed to decision (early escalation — P(consensus|remaining) below threshold).
41
41
  7. Include the token <!-- GSD_DECISION --> in your FINAL output (only when delivering
42
42
  the completed plan, research, verification report, or filtered question list)
@@ -55,16 +55,16 @@ function appendConformanceEvent(event) {
55
55
  const logPath = pp.resolve(process.cwd(), 'conformance-events');
56
56
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n', 'utf8');
57
57
  } catch (err) {
58
- process.stderr.write('[qgsd] conformance log write failed: ' + err.message + '\n');
58
+ process.stderr.write('[nf] conformance log write failed: ' + err.message + '\n');
59
59
  }
60
60
  }
61
61
 
62
62
  // Locate the oscillation-resolution-mode workflow.
63
- // Tries global install path first (~/.claude/qgsd/), then local (.claude/qgsd/).
63
+ // Tries global install path first (~/.claude/nf/), then local (.claude/nf/).
64
64
  function findResolutionWorkflow(cwd) {
65
65
  const candidates = [
66
- path.join(os.homedir(), '.claude', 'qgsd', 'workflows', 'oscillation-resolution-mode.md'),
67
- path.join(cwd, '.claude', 'qgsd', 'workflows', 'oscillation-resolution-mode.md'),
66
+ path.join(os.homedir(), '.claude', 'nf', 'workflows', 'oscillation-resolution-mode.md'),
67
+ path.join(cwd, '.claude', 'nf', 'workflows', 'oscillation-resolution-mode.md'),
68
68
  ];
69
69
  for (const p of candidates) {
70
70
  if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
@@ -150,7 +150,7 @@ function getRecentlyTimedOutSlots(cwd, ttlMinutes = 30) {
150
150
  function findProviders() {
151
151
  const searchPaths = [
152
152
  path.join(__dirname, '..', 'bin', 'providers.json'),
153
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'providers.json'),
153
+ path.join(os.homedir(), '.claude', 'nf-bin', 'providers.json'),
154
154
  ];
155
155
  for (const p of searchPaths) {
156
156
  try {
@@ -169,7 +169,7 @@ function triggerHealthProbe() {
169
169
  try {
170
170
  const searchPaths = [
171
171
  path.join(__dirname, '..', 'bin', 'check-provider-health.cjs'),
172
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'check-provider-health.cjs'),
172
+ path.join(os.homedir(), '.claude', 'nf-bin', 'check-provider-health.cjs'),
173
173
  ];
174
174
  let checkPath = null;
175
175
  for (const p of searchPaths) {
@@ -199,7 +199,7 @@ function getAvailableSlots(slots, cwd) {
199
199
  const ts = new Date(avail.available_at_iso).getTime();
200
200
  if (isNaN(ts)) return true; // malformed date: fail-open
201
201
  if (ts > now) {
202
- console.error(`[qgsd-dispatch] AVAILABILITY EXCLUDE: ${s.slot} -- available_at_iso=${avail.available_at_iso} is in the future (now=${new Date().toISOString()})`);
202
+ console.error(`[nf-dispatch] AVAILABILITY EXCLUDE: ${s.slot} -- available_at_iso=${avail.available_at_iso} is in the future (now=${new Date().toISOString()})`);
203
203
  return false;
204
204
  }
205
205
  return true;
@@ -254,18 +254,18 @@ function sortBySuccessRate(slots, cwd) {
254
254
  return getRate(b.slot) - getRate(a.slot);
255
255
  });
256
256
 
257
- console.error('[qgsd-dispatch] DISPATCH ORDER (flakiness,rate): [' +
257
+ console.error('[nf-dispatch] DISPATCH ORDER (flakiness,rate): [' +
258
258
  sorted.map(s => `${s.slot}(f=${getFlakiness(s.slot).toFixed(2)},r=${getRate(s.slot).toFixed(3)})`).join(', ') + ']');
259
259
  return sorted;
260
260
  } catch (_) { return [...slots]; } // fail-open: any error → return original order
261
261
  }
262
262
 
263
263
  // Returns slot names whose backing provider is DOWN (probed unhealthy).
264
- // Reads ~/.claude/qgsd-provider-cache.json and matches providers from providers.json.
264
+ // Reads ~/.claude/nf-provider-cache.json and matches providers from providers.json.
265
265
  // When a provider's endpoint is DOWN, ALL slots backed by that provider are skipped.
266
266
  function getDownProviderSlots() {
267
267
  try {
268
- const cachePath = path.join(os.homedir(), '.claude', 'qgsd-provider-cache.json');
268
+ const cachePath = path.join(os.homedir(), '.claude', 'nf-provider-cache.json');
269
269
  if (!fs.existsSync(cachePath)) return [];
270
270
  const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
271
271
  if (!cache || !cache.entries) return [];
@@ -342,7 +342,7 @@ process.stdin.on('end', () => {
342
342
  const workflow = findResolutionWorkflow(cwd);
343
343
  const context = workflow
344
344
  ? `CIRCUIT BREAKER ACTIVE — OSCILLATION RESOLUTION MODE\n\nYou MUST follow this procedure immediately before doing anything else:\n\n${workflow}`
345
- : `CIRCUIT BREAKER ACTIVE — OSCILLATION RESOLUTION MODE\n\nOscillation has been detected in recent commits. Tool calls are NOT blocked — you can still read and write files — but you MUST resolve the oscillation before making further commits.\nFollow the oscillation resolution procedure in R5 of CLAUDE.md:\n1. Run: git log --oneline --name-only -6 to identify the oscillating file set.\n2. Run quorum diagnosis with structural coupling framing.\n3. Present unified solution to user for approval.\n4. Do NOT commit until user approves AND runs: npx qgsd --reset-breaker`;
345
+ : `CIRCUIT BREAKER ACTIVE — OSCILLATION RESOLUTION MODE\n\nOscillation has been detected in recent commits. Tool calls are NOT blocked — you can still read and write files — but you MUST resolve the oscillation before making further commits.\nFollow the oscillation resolution procedure in R5 of CLAUDE.md:\n1. Run: git log --oneline --name-only -6 to identify the oscillating file set.\n2. Run quorum diagnosis with structural coupling framing.\n3. Present unified solution to user for approval.\n4. Do NOT commit until user approves AND runs: npx nforma --reset-breaker`;
346
346
  process.stdout.write(JSON.stringify({
347
347
  hookSpecificOutput: {
348
348
  hookEventName: 'UserPromptSubmit',
@@ -358,7 +358,7 @@ process.stdin.on('end', () => {
358
358
  process.stdout.write(JSON.stringify({
359
359
  hookSpecificOutput: {
360
360
  hookEventName: 'UserPromptSubmit',
361
- additionalContext: `PENDING QUEUED TASK — Execute this immediately before anything else:\n\n${pendingTask}\n\n(This task was queued via /qgsd:queue before the previous /clear.)`,
361
+ additionalContext: `PENDING QUEUED TASK — Execute this immediately before anything else:\n\n${pendingTask}\n\n(This task was queued via /nf:queue before the previous /clear.)`,
362
362
  }
363
363
  }));
364
364
  process.exit(0);
@@ -366,6 +366,13 @@ process.stdin.on('end', () => {
366
366
 
367
367
  // ── Priority 3: Planning command → inject quorum instructions ────────────
368
368
  const config = loadConfig(cwd);
369
+
370
+ // Profile guard — exit early if this hook is not active for the current profile
371
+ const profile = config.hook_profile || 'standard';
372
+ if (!shouldRunHook('nf-prompt', profile)) {
373
+ process.exit(0);
374
+ }
375
+
369
376
  const commands = config.quorum_commands;
370
377
 
371
378
  // Parse --n N override from the raw prompt
@@ -380,7 +387,7 @@ process.stdin.on('end', () => {
380
387
 
381
388
  // Solo mode: --n 1 means Claude-only quorum — bypass all external slot dispatches
382
389
  if (quorumSizeOverride === 1) {
383
- instructions = `<!-- QGSD_SOLO_MODE -->\nSOLO MODE ACTIVE (--n 1): Self-quorum only. Skip ALL external slot-worker Task dispatches. Claude's vote is the quorum. Write <!-- GSD_DECISION --> in your final output. The Stop hook is informed.\n\n`;
390
+ instructions = `<!-- NF_SOLO_MODE -->\nSOLO MODE ACTIVE (--n 1): Self-quorum only. Skip ALL external slot-worker Task dispatches. Claude's vote is the quorum. Write <!-- GSD_DECISION --> in your final output. The Stop hook is informed.\n\n`;
384
391
  } else if (config.quorum_instructions) {
385
392
  // Explicit quorum_instructions in config — use as-is
386
393
  instructions = config.quorum_instructions;
@@ -427,8 +434,8 @@ process.stdin.on('end', () => {
427
434
  // Guard: empty roster — no external agents configured at all
428
435
  if (orderedSlots.length === 0) {
429
436
  // Fail-open to solo mode: Claude is the only quorum participant
430
- console.error('[qgsd-dispatch] WARNING: no external agents in roster — falling back to solo quorum');
431
- instructions = `<!-- QGSD_SOLO_MODE -->\nSOLO MODE ACTIVE (empty roster): No external agents configured in providers.json or quorum_active. Claude's vote is the quorum. Write <!-- GSD_DECISION --> in your final output. The Stop hook is informed.\n\nTo add agents, run /qgsd:mcp-setup or edit ~/.claude/qgsd.json quorum_active.\n`;
437
+ console.error('[nf-dispatch] WARNING: no external agents in roster — falling back to solo quorum');
438
+ instructions = `<!-- NF_SOLO_MODE -->\nSOLO MODE ACTIVE (empty roster): No external agents configured in providers.json or quorum_active. Claude's vote is the quorum. Write <!-- GSD_DECISION --> in your final output. The Stop hook is informed.\n\nTo add agents, run /nf:mcp-setup or edit ~/.claude/nf.json quorum_active.\n`;
432
439
  } else {
433
440
  if (preferSub) {
434
441
  orderedSlots.sort((a, b) => {
@@ -460,6 +467,54 @@ process.stdin.on('end', () => {
460
467
  // DISP-03: Sort by descending success rate
461
468
  cappedSlots = sortBySuccessRate(cappedSlots, cwd);
462
469
 
470
+ // ── CACHE CHECK: Short-circuit quorum dispatch on valid cache hit ──────
471
+ try {
472
+ const cacheModule = require(path.join(__dirname, '..', 'bin', 'quorum-cache.cjs'));
473
+ const cacheDir = path.join(cwd, '.planning', '.quorum-cache');
474
+ const cacheKey = cacheModule.computeCacheKey(prompt, contextYaml, cappedSlots, config.quorum_active, cacheModule.getGitHead());
475
+
476
+ const cachedEntry = cacheModule.readCache(cacheKey, cacheDir);
477
+ if (cachedEntry && cacheModule.isCacheValid(cachedEntry, cacheModule.getGitHead(), config.quorum_active || [])) {
478
+ // Cache hit — serve cached result without dispatching slots
479
+ const ageMs = Date.now() - new Date(cachedEntry.created).getTime();
480
+ const timeAgo = ageMs < 3600000
481
+ ? Math.round(ageMs / 60000) + 'm ago'
482
+ : Math.round(ageMs / 3600000) + 'h ago';
483
+
484
+ appendConformanceEvent({
485
+ ts: new Date().toISOString(),
486
+ phase: 'DECIDING',
487
+ action: 'cache_hit',
488
+ cache_key: cacheKey.slice(0, 12),
489
+ slots_available: cachedEntry.slot_count,
490
+ vote_result: cachedEntry.vote_result,
491
+ outcome: 'APPROVE',
492
+ schema_version,
493
+ });
494
+
495
+ const cacheInstructions = `<!-- NF_CACHE_HIT -->\n<!-- NF_CACHE_KEY:${cacheKey} -->\nQUORUM CACHE HIT: Identical dispatch was completed ${timeAgo}.\nCached result: ${cachedEntry.vote_result} of ${cachedEntry.slot_count} slots approved.\nDecision: ${cachedEntry.outcome}\nSkip all slot-worker Task dispatches. Use this cached quorum result.\nInclude <!-- GSD_DECISION --> in your final output.`;
496
+
497
+ process.stdout.write(JSON.stringify({
498
+ hookSpecificOutput: {
499
+ hookEventName: 'UserPromptSubmit',
500
+ additionalContext: cacheInstructions,
501
+ }
502
+ }));
503
+ process.exit(0);
504
+ }
505
+
506
+ // Cache miss — store key for embedding in instructions and pending entry write
507
+ var _nfCacheKey = cacheKey;
508
+ var _nfCacheModule = cacheModule;
509
+ var _nfCacheDir = cacheDir;
510
+ } catch (cacheErr) {
511
+ // Fail-open: cache errors never prevent normal quorum dispatch
512
+ process.stderr.write('[nf] cache check failed (fail-open): ' + (cacheErr.message || cacheErr) + '\n');
513
+ var _nfCacheKey = null;
514
+ var _nfCacheModule = null;
515
+ var _nfCacheDir = null;
516
+ }
517
+
463
518
  // SC-4: Graceful fallback — ensure at least one slot in dispatch list
464
519
  if (cappedSlots.length === 0 && orderedSlots.length > 0) {
465
520
  const relaxedSlots = orderedSlots.filter(s => !skipSet.has(s.slot));
@@ -468,7 +523,7 @@ process.stdin.on('end', () => {
468
523
  } else {
469
524
  cappedSlots = [orderedSlots[0]]; // last resort: any slot at all
470
525
  }
471
- console.error(`[qgsd-dispatch] FALLBACK: all slots filtered, restored ${cappedSlots[0].slot}`);
526
+ console.error(`[nf-dispatch] FALLBACK: all slots filtered, restored ${cappedSlots[0].slot}`);
472
527
  }
473
528
 
474
529
  // Generate step list, with optional section headers when preferSub is on
@@ -481,7 +536,7 @@ process.stdin.on('end', () => {
481
536
  stepLines.push(' [API agents — overflow if sub count insufficient]');
482
537
  inApiSection = true;
483
538
  }
484
- stepLines.push(` ${stepNum}. Task(subagent_type="qgsd-quorum-slot-worker", prompt="slot: ${slot}\\nround: 1\\ntimeout_ms: 60000\\nrepo_dir: <cwd>\\nmode: A\\nquestion: <question>")`);
539
+ stepLines.push(` ${stepNum}. Task(subagent_type="nf-quorum-slot-worker", prompt="slot: ${slot}\\nround: 1\\ntimeout_ms: 60000\\nrepo_dir: <cwd>\\nmode: A\\nquestion: <question>")`);
485
540
  stepNum++;
486
541
  }
487
542
  const dynamicSteps = stepLines.join('\n');
@@ -559,18 +614,18 @@ process.stdin.on('end', () => {
559
614
  }
560
615
 
561
616
  instructions = `QUORUM REQUIRED${minNote} (structural enforcement — Stop hook will verify)\n\n` +
562
- `Run the full R3 quorum protocol inline (dispatch_pattern from commands/qgsd/quorum.md):\n` +
563
- `Dispatch ALL active slots as parallel sibling qgsd-quorum-slot-worker Tasks in ONE message turn.\n` +
564
- `NEVER call mcp__*__* tools directly — use Task(subagent_type="qgsd-quorum-slot-worker") ONLY:\n` +
617
+ `Run the full R3 quorum protocol inline (dispatch_pattern from commands/nf/quorum.md):\n` +
618
+ `Dispatch ALL active slots as parallel sibling nf-quorum-slot-worker Tasks in ONE message turn.\n` +
619
+ `NEVER call mcp__*__* tools directly — use Task(subagent_type="nf-quorum-slot-worker") ONLY:\n` +
565
620
  (hasMixed ? ' [Subscription agents — preferred, flat-fee]\n' : '') +
566
621
  dynamicSteps + '\n\n' +
567
622
  skipNote +
568
623
  failoverRule + '\n\n' +
569
624
  `After quorum:\n` +
570
625
  ` ${afterSteps}. Synthesize results inline. Deliberate up to 10 rounds per R3.3 if no consensus.\n` +
571
- ` ${afterSteps + 1}. Update scoreboard: node ~/.claude/qgsd-bin/update-scoreboard.cjs merge-wave ...\n` +
626
+ ` ${afterSteps + 1}. Update scoreboard: node ~/.claude/nf-bin/update-scoreboard.cjs merge-wave ...\n` +
572
627
  ` ${afterSteps + 2}. [HEAL-01] After EACH deliberation round's merge-wave, check early escalation:\n` +
573
- ` node ~/.claude/qgsd-bin/quorum-consensus-gate.cjs --min-quorum=2 --remaining-rounds=R\n` +
628
+ ` node ~/.claude/nf-bin/quorum-consensus-gate.cjs --min-quorum=2 --remaining-rounds=R\n` +
574
629
  ` where R = (maxDeliberation - currentRound). For example, on round 2 of 7 max: --remaining-rounds=5.\n` +
575
630
  ` If exit code 1 (shouldEscalate=true, P(consensus|remaining) below 10% threshold), stop deliberating and proceed to decision immediately.\n` +
576
631
  ` This prevents wasting rounds when consensus is mathematically unlikely.\n` +
@@ -607,17 +662,39 @@ process.stdin.on('end', () => {
607
662
  const tool = AGENT_TOOL_MAP[agent] || ('mcp__' + agent);
608
663
  return ' - When calling ' + tool + ', include model="' + model + '" in the tool input';
609
664
  }).join('\n');
610
- instructions += '\n\nModel overrides (from qgsd.json model_preferences):\n' +
665
+ instructions += '\n\nModel overrides (from nf.json model_preferences):\n' +
611
666
  'The following agents have preferred models configured. Pass the specified model parameter:\n' +
612
667
  lines;
613
668
  }
614
669
 
615
- // Anchored allowlist — requires /gsd: or /qgsd: prefix and word boundary after command name.
616
- const cmdPattern = new RegExp('^\\s*\\/q?gsd:(' + commands.join('|') + ')(\\s|$)');
670
+ // Anchored allowlist — requires /nf:, /gsd:, or /qgsd: prefix and word boundary after command name.
671
+ // Strict mode: match ANY /nf: or /gsd: or /qgsd: command, not just quorum_commands list.
672
+ const cmdPattern = profile === 'strict'
673
+ ? /^\s*\/(nf|q?gsd):[\w][\w-]*(\s|$)/
674
+ : new RegExp('^\\s*\\/(nf|q?gsd):(' + commands.join('|') + ')(\\s|$)');
617
675
  if (!cmdPattern.test(prompt)) {
618
676
  process.exit(0); // Silent pass — UPS-05
619
677
  }
620
678
 
679
+ // ── CACHE MISS: Embed cache key marker and write pending entry ──────────
680
+ if (typeof _nfCacheKey === 'string' && _nfCacheKey && _nfCacheModule && _nfCacheDir) {
681
+ try {
682
+ instructions = `<!-- NF_CACHE_KEY:${_nfCacheKey} -->\n` + instructions;
683
+ _nfCacheModule.writeCache(_nfCacheKey, {
684
+ version: 1,
685
+ key: _nfCacheKey,
686
+ created: new Date().toISOString(),
687
+ ttl_ms: (config.cache_ttl_ms || 3600000),
688
+ git_head: _nfCacheModule.getGitHead(),
689
+ quorum_active: (config.quorum_active || []).slice(),
690
+ slot_count: cappedSlots ? cappedSlots.length : 0,
691
+ }, _nfCacheDir);
692
+ } catch (pendingErr) {
693
+ // Fail-open: pending entry write failure never blocks dispatch
694
+ process.stderr.write('[nf] cache pending write failed (fail-open): ' + (pendingErr.message || pendingErr) + '\n');
695
+ }
696
+ }
697
+
621
698
  appendConformanceEvent({
622
699
  ts: new Date().toISOString(),
623
700
  phase: 'IDLE',