@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.
Files changed (201) 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-slot-dispatch.cjs +6 -6
  85. package/bin/requirements-core.cjs +1 -1
  86. package/bin/review-mcp-logs.cjs +1 -1
  87. package/bin/risk-heatmap.cjs +151 -0
  88. package/bin/run-account-manager-tlc.cjs +4 -4
  89. package/bin/run-account-pool-alloy.cjs +2 -2
  90. package/bin/run-alloy.cjs +2 -2
  91. package/bin/run-audit-alloy.cjs +2 -2
  92. package/bin/run-breaker-tlc.cjs +3 -3
  93. package/bin/run-formal-check.cjs +9 -9
  94. package/bin/run-formal-verify.cjs +30 -9
  95. package/bin/run-installer-alloy.cjs +2 -2
  96. package/bin/run-oscillation-tlc.cjs +4 -4
  97. package/bin/run-phase-tlc.cjs +1 -1
  98. package/bin/run-protocol-tlc.cjs +4 -4
  99. package/bin/run-quorum-composition-alloy.cjs +2 -2
  100. package/bin/run-sensitivity-sweep.cjs +2 -2
  101. package/bin/run-stop-hook-tlc.cjs +3 -3
  102. package/bin/run-tlc.cjs +21 -21
  103. package/bin/run-transcript-alloy.cjs +2 -2
  104. package/bin/secrets.cjs +5 -5
  105. package/bin/security-sweep.cjs +238 -0
  106. package/bin/sensitivity-report.cjs +3 -3
  107. package/bin/set-secret.cjs +5 -5
  108. package/bin/setup-telemetry-cron.sh +3 -3
  109. package/bin/stall-detector.cjs +126 -0
  110. package/bin/state-candidates.cjs +206 -0
  111. package/bin/sync-baseline-requirements.cjs +1 -1
  112. package/bin/telemetry-collector.cjs +1 -1
  113. package/bin/test-changed.cjs +111 -0
  114. package/bin/test-recipe-gen.cjs +250 -0
  115. package/bin/trace-corpus-stats.cjs +211 -0
  116. package/bin/unified-mcp-server.mjs +3 -3
  117. package/bin/update-scoreboard.cjs +1 -1
  118. package/bin/validate-memory.cjs +2 -2
  119. package/bin/validate-traces.cjs +10 -10
  120. package/bin/verify-quorum-health.cjs +66 -5
  121. package/bin/xstate-to-tla.cjs +4 -4
  122. package/bin/xstate-trace-walker.cjs +3 -3
  123. package/commands/{qgsd → nf}/add-phase.md +3 -3
  124. package/commands/{qgsd → nf}/add-requirement.md +3 -3
  125. package/commands/{qgsd → nf}/add-todo.md +3 -3
  126. package/commands/{qgsd → nf}/audit-milestone.md +4 -4
  127. package/commands/{qgsd → nf}/check-todos.md +3 -3
  128. package/commands/{qgsd → nf}/cleanup.md +3 -3
  129. package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
  130. package/commands/{qgsd → nf}/complete-milestone.md +9 -9
  131. package/commands/{qgsd → nf}/debug.md +9 -9
  132. package/commands/{qgsd → nf}/discuss-phase.md +3 -3
  133. package/commands/{qgsd → nf}/execute-phase.md +15 -15
  134. package/commands/{qgsd → nf}/fix-tests.md +3 -3
  135. package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
  136. package/commands/{qgsd → nf}/health.md +3 -3
  137. package/commands/{qgsd → nf}/help.md +3 -3
  138. package/commands/{qgsd → nf}/insert-phase.md +3 -3
  139. package/commands/nf/join-discord.md +18 -0
  140. package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
  141. package/commands/{qgsd → nf}/map-codebase.md +7 -7
  142. package/commands/{qgsd → nf}/map-requirements.md +3 -3
  143. package/commands/{qgsd → nf}/mcp-restart.md +3 -3
  144. package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
  145. package/commands/{qgsd → nf}/mcp-setup.md +63 -63
  146. package/commands/{qgsd → nf}/mcp-status.md +3 -3
  147. package/commands/{qgsd → nf}/mcp-update.md +7 -7
  148. package/commands/{qgsd → nf}/new-milestone.md +8 -8
  149. package/commands/{qgsd → nf}/new-project.md +8 -8
  150. package/commands/{qgsd → nf}/observe.md +49 -16
  151. package/commands/{qgsd → nf}/pause-work.md +3 -3
  152. package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
  153. package/commands/{qgsd → nf}/plan-phase.md +6 -6
  154. package/commands/{qgsd → nf}/polyrepo.md +2 -2
  155. package/commands/{qgsd → nf}/progress.md +3 -3
  156. package/commands/{qgsd → nf}/queue.md +2 -2
  157. package/commands/{qgsd → nf}/quick.md +8 -8
  158. package/commands/{qgsd → nf}/quorum-test.md +10 -10
  159. package/commands/{qgsd → nf}/quorum.md +40 -40
  160. package/commands/{qgsd → nf}/reapply-patches.md +2 -2
  161. package/commands/{qgsd → nf}/remove-phase.md +3 -3
  162. package/commands/{qgsd → nf}/research-phase.md +12 -12
  163. package/commands/{qgsd → nf}/resume-work.md +3 -3
  164. package/commands/nf/review-requirements.md +31 -0
  165. package/commands/{qgsd → nf}/set-profile.md +3 -3
  166. package/commands/{qgsd → nf}/settings.md +6 -6
  167. package/commands/{qgsd → nf}/solve.md +35 -35
  168. package/commands/{qgsd → nf}/sync-baselines.md +4 -4
  169. package/commands/{qgsd → nf}/triage.md +10 -10
  170. package/commands/{qgsd → nf}/update.md +3 -3
  171. package/commands/{qgsd → nf}/verify-work.md +5 -5
  172. package/hooks/dist/config-loader.js +188 -32
  173. package/hooks/dist/conformance-schema.cjs +2 -2
  174. package/hooks/dist/gsd-context-monitor.js +118 -13
  175. package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
  176. package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
  177. package/hooks/dist/nf-circuit-breaker.test.js +1002 -0
  178. package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
  179. package/hooks/dist/nf-precompact.test.js +227 -0
  180. package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
  181. package/hooks/dist/nf-prompt.test.js +698 -0
  182. package/hooks/dist/nf-session-start.js +185 -0
  183. package/hooks/dist/nf-session-start.test.js +354 -0
  184. package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
  185. package/hooks/dist/nf-slot-correlator.test.js +85 -0
  186. package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
  187. package/hooks/dist/nf-spec-regen.test.js +73 -0
  188. package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
  189. package/hooks/dist/nf-statusline.test.js +157 -0
  190. package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
  191. package/hooks/dist/nf-stop.test.js +1388 -0
  192. package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
  193. package/hooks/dist/nf-token-collector.test.js +262 -0
  194. package/hooks/dist/unified-mcp-server.mjs +2 -2
  195. package/package.json +4 -4
  196. package/scripts/build-hooks.js +13 -6
  197. package/scripts/secret-audit.sh +1 -1
  198. package/scripts/verify-hooks-sync.cjs +90 -0
  199. package/templates/{qgsd.json → nf.json} +4 -4
  200. package/commands/qgsd/join-discord.md +0 -18
  201. package/hooks/dist/qgsd-session-start.js +0 -122
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // Exports: loadConfig(projectDir?), DEFAULT_CONFIG
6
6
  //
7
- // Load order: DEFAULT_CONFIG → ~/.claude/qgsd.json (global) → .claude/qgsd.json in projectDir (project)
7
+ // Load order: DEFAULT_CONFIG → ~/.claude/nf.json (global) → .claude/nf.json in projectDir (project)
8
8
  // Merge: shallow spread — project values fully replace global values for any overlapping key.
9
9
  // Warnings: all written to process.stderr — stdout is never touched (it is the hook decision channel).
10
10
 
@@ -15,7 +15,7 @@ const path = require('path');
15
15
  const os = require('os');
16
16
 
17
17
  // Maps the family name of a slot (trailing -N stripped) to the MCP tool suffix to call.
18
- // Used by both qgsd-prompt.js (step generation) and qgsd-stop.js (evidence detection).
18
+ // Used by both nf-prompt.js (step generation) and nf-stop.js (evidence detection).
19
19
  const SLOT_TOOL_SUFFIX = {
20
20
  'codex-cli': 'review',
21
21
  'codex': 'review',
@@ -35,6 +35,42 @@ function slotToToolCall(slotName) {
35
35
  return 'mcp__' + slotName + '__' + suffix;
36
36
  }
37
37
 
38
+ const HOOK_PROFILE_MAP = {
39
+ minimal: new Set([
40
+ 'nf-circuit-breaker',
41
+ 'nf-precompact',
42
+ ]),
43
+ standard: new Set([
44
+ 'nf-circuit-breaker',
45
+ 'nf-precompact',
46
+ 'nf-prompt',
47
+ 'nf-stop',
48
+ 'gsd-context-monitor',
49
+ 'nf-spec-regen',
50
+ 'nf-token-collector',
51
+ 'nf-slot-correlator',
52
+ 'nf-session-start',
53
+ 'nf-statusline',
54
+ ]),
55
+ strict: new Set([
56
+ 'nf-circuit-breaker',
57
+ 'nf-precompact',
58
+ 'nf-prompt',
59
+ 'nf-stop',
60
+ 'gsd-context-monitor',
61
+ 'nf-spec-regen',
62
+ 'nf-token-collector',
63
+ 'nf-slot-correlator',
64
+ 'nf-session-start',
65
+ 'nf-statusline',
66
+ ]),
67
+ };
68
+
69
+ function shouldRunHook(hookBasename, profile) {
70
+ const validProfile = HOOK_PROFILE_MAP[profile] ? profile : 'standard';
71
+ return HOOK_PROFILE_MAP[validProfile].has(hookBasename);
72
+ }
73
+
38
74
  const DEFAULT_CONFIG = {
39
75
  quorum_commands: [
40
76
  'plan-phase', 'new-project', 'new-milestone',
@@ -72,6 +108,20 @@ const DEFAULT_CONFIG = {
72
108
  warn_pct: 70,
73
109
  critical_pct: 90,
74
110
  },
111
+ budget: {
112
+ session_limit_tokens: null, // null = disabled (fail-open)
113
+ warn_pct: 60, // inject warning at this % of session_limit_tokens
114
+ downgrade_pct: 85, // auto-downgrade model profile at this %
115
+ },
116
+ stall_detection: {
117
+ timeout_s: 90, // mark slot stalled after this many seconds
118
+ consecutive_threshold: 2, // require N consecutive stalled dispatches
119
+ check_commits: true, // only escalate if no new commits
120
+ },
121
+ smart_compact: {
122
+ enabled: true, // master switch
123
+ context_warn_pct: 60, // suggest compact above this context usage %
124
+ },
75
125
  // quorum_active: array of slot names that participate in quorum.
76
126
  // [] = all discovered slots participate (fail-open, backward compatible with pre-Phase-40 installs).
77
127
  // A non-empty array is an explicit allowlist.
@@ -86,6 +136,7 @@ const DEFAULT_CONFIG = {
86
136
  // task_envelope_enabled: master switch for task-envelope.json sidecar writes.
87
137
  // Flat key required — nested objects lost in shallow merge.
88
138
  task_envelope_enabled: true,
139
+ hook_profile: 'standard',
89
140
  };
90
141
 
91
142
  // Reads and parses a JSON config file.
@@ -97,7 +148,7 @@ function readConfigFile(filePath) {
97
148
  try {
98
149
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
99
150
  } catch (e) {
100
- process.stderr.write('[qgsd] WARNING: Malformed config at ' + filePath + ': ' + e.message + '\n');
151
+ process.stderr.write('[nf] WARNING: Malformed config at ' + filePath + ': ' + e.message + '\n');
101
152
  return null;
102
153
  }
103
154
  }
@@ -107,33 +158,33 @@ function readConfigFile(filePath) {
107
158
  // Returns the (possibly corrected) config object.
108
159
  function validateConfig(config) {
109
160
  if (!Array.isArray(config.quorum_commands)) {
110
- process.stderr.write('[qgsd] WARNING: qgsd.json: quorum_commands must be an array; using defaults\n');
161
+ process.stderr.write('[nf] WARNING: nf.json: quorum_commands must be an array; using defaults\n');
111
162
  config.quorum_commands = DEFAULT_CONFIG.quorum_commands;
112
163
  }
113
164
 
114
165
  if (typeof config.required_models !== 'object' || config.required_models === null) {
115
- process.stderr.write('[qgsd] WARNING: qgsd.json: required_models must be an object; using defaults\n');
166
+ process.stderr.write('[nf] WARNING: nf.json: required_models must be an object; using defaults\n');
116
167
  config.required_models = DEFAULT_CONFIG.required_models;
117
168
  }
118
169
 
119
170
  if (!['open', 'closed'].includes(config.fail_mode)) {
120
- process.stderr.write('[qgsd] WARNING: qgsd.json: fail_mode "' + config.fail_mode + '" invalid; defaulting to "open"\n');
171
+ process.stderr.write('[nf] WARNING: nf.json: fail_mode "' + config.fail_mode + '" invalid; defaulting to "open"\n');
121
172
  config.fail_mode = 'open';
122
173
  }
123
174
 
124
175
  // Validate circuit_breaker sub-object
125
176
  if (typeof config.circuit_breaker !== 'object' || config.circuit_breaker === null) {
126
- process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker must be an object; using defaults\n');
177
+ process.stderr.write('[nf] WARNING: nf.json: circuit_breaker must be an object; using defaults\n');
127
178
  config.circuit_breaker = { ...DEFAULT_CONFIG.circuit_breaker };
128
179
  } else {
129
180
  // Validate oscillation_depth independently
130
181
  if (!Number.isInteger(config.circuit_breaker.oscillation_depth) || config.circuit_breaker.oscillation_depth < 1) {
131
- process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.oscillation_depth must be a positive integer; defaulting to 3\n');
182
+ process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.oscillation_depth must be a positive integer; defaulting to 3\n');
132
183
  config.circuit_breaker.oscillation_depth = 3;
133
184
  }
134
185
  // Validate commit_window independently
135
186
  if (!Number.isInteger(config.circuit_breaker.commit_window) || config.circuit_breaker.commit_window < 1) {
136
- process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.commit_window must be a positive integer; defaulting to 6\n');
187
+ process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.commit_window must be a positive integer; defaulting to 6\n');
137
188
  config.circuit_breaker.commit_window = 6;
138
189
  }
139
190
  // Fill in missing sub-keys with defaults (handles partial circuit_breaker objects)
@@ -150,18 +201,18 @@ function validateConfig(config) {
150
201
  config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
151
202
  }
152
203
  if (typeof config.circuit_breaker.haiku_reviewer !== 'boolean') {
153
- process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.haiku_reviewer must be boolean; defaulting to true\n');
204
+ process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.haiku_reviewer must be boolean; defaulting to true\n');
154
205
  config.circuit_breaker.haiku_reviewer = true;
155
206
  }
156
207
  if (typeof config.circuit_breaker.haiku_model !== 'string') {
157
- process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.haiku_model must be a string; using default\n');
208
+ process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.haiku_model must be a string; using default\n');
158
209
  config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
159
210
  }
160
211
  }
161
212
 
162
213
  // Validate quorum_active
163
214
  if (!Array.isArray(config.quorum_active)) {
164
- process.stderr.write('[qgsd] WARNING: qgsd.json: quorum_active must be an array; using []\n');
215
+ process.stderr.write('[nf] WARNING: nf.json: quorum_active must be an array; using []\n');
165
216
  config.quorum_active = [];
166
217
  } else {
167
218
  config.quorum_active = config.quorum_active.filter(
@@ -174,7 +225,7 @@ function validateConfig(config) {
174
225
  config.quorum = { ...DEFAULT_CONFIG.quorum };
175
226
  } else {
176
227
  if (!Number.isInteger(config.quorum.minSize) || config.quorum.minSize < 1) {
177
- process.stderr.write('[qgsd] WARNING: qgsd.json: quorum.minSize must be a positive integer; defaulting to 4\n');
228
+ process.stderr.write('[nf] WARNING: nf.json: quorum.minSize must be a positive integer; defaulting to 4\n');
178
229
  config.quorum.minSize = DEFAULT_CONFIG.quorum.minSize;
179
230
  }
180
231
  if (typeof config.quorum.preferSub !== 'boolean') {
@@ -184,15 +235,15 @@ function validateConfig(config) {
184
235
 
185
236
  // Validate agent_config
186
237
  if (typeof config.agent_config !== 'object' || config.agent_config === null || Array.isArray(config.agent_config)) {
187
- process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config must be an object; using {}\n');
238
+ process.stderr.write('[nf] WARNING: nf.json: agent_config must be an object; using {}\n');
188
239
  config.agent_config = {};
189
240
  } else {
190
241
  for (const [slot, meta] of Object.entries(config.agent_config)) {
191
242
  if (typeof meta !== 'object' || meta === null) {
192
- process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config.' + slot + ' must be an object; removing\n');
243
+ process.stderr.write('[nf] WARNING: nf.json: agent_config.' + slot + ' must be an object; removing\n');
193
244
  delete config.agent_config[slot];
194
245
  } else if (meta.auth_type && !['sub', 'api'].includes(meta.auth_type)) {
195
- process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config.' + slot + '.auth_type must be "sub" or "api"; defaulting to "api"\n');
246
+ process.stderr.write('[nf] WARNING: nf.json: agent_config.' + slot + '.auth_type must be "sub" or "api"; defaulting to "api"\n');
196
247
  meta.auth_type = 'api';
197
248
  }
198
249
  }
@@ -200,13 +251,13 @@ function validateConfig(config) {
200
251
 
201
252
  // Validate model_preferences
202
253
  if (typeof config.model_preferences !== 'object' || config.model_preferences === null || Array.isArray(config.model_preferences)) {
203
- process.stderr.write('[qgsd] WARNING: qgsd.json: model_preferences must be an object; using {}\n');
254
+ process.stderr.write('[nf] WARNING: nf.json: model_preferences must be an object; using {}\n');
204
255
  config.model_preferences = {};
205
256
  } else {
206
257
  // Remove invalid entries (non-string values) with a warning
207
258
  for (const [key, val] of Object.entries(config.model_preferences)) {
208
259
  if (typeof val !== 'string' || val.trim() === '') {
209
- process.stderr.write('[qgsd] WARNING: qgsd.json: model_preferences.' + key + ' must be a non-empty string; removing\n');
260
+ process.stderr.write('[nf] WARNING: nf.json: model_preferences.' + key + ' must be a non-empty string; removing\n');
210
261
  delete config.model_preferences[key];
211
262
  }
212
263
  }
@@ -214,21 +265,21 @@ function validateConfig(config) {
214
265
 
215
266
  // Validate context_monitor sub-object
216
267
  if (typeof config.context_monitor !== 'object' || config.context_monitor === null) {
217
- process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor must be an object; using defaults\n');
268
+ process.stderr.write('[nf] WARNING: nf.json: context_monitor must be an object; using defaults\n');
218
269
  config.context_monitor = { ...DEFAULT_CONFIG.context_monitor };
219
270
  } else {
220
271
  if (!Number.isInteger(config.context_monitor.warn_pct) ||
221
272
  config.context_monitor.warn_pct < 1 || config.context_monitor.warn_pct > 99) {
222
- process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.warn_pct must be an integer 1-99; defaulting to 70\n');
273
+ process.stderr.write('[nf] WARNING: nf.json: context_monitor.warn_pct must be an integer 1-99; defaulting to 70\n');
223
274
  config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
224
275
  }
225
276
  if (!Number.isInteger(config.context_monitor.critical_pct) ||
226
277
  config.context_monitor.critical_pct < 1 || config.context_monitor.critical_pct > 100) {
227
- process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.critical_pct must be an integer 1-100; defaulting to 90\n');
278
+ process.stderr.write('[nf] WARNING: nf.json: context_monitor.critical_pct must be an integer 1-100; defaulting to 90\n');
228
279
  config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
229
280
  }
230
281
  if (config.context_monitor.warn_pct >= config.context_monitor.critical_pct) {
231
- process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.warn_pct must be less than critical_pct; resetting to defaults\n');
282
+ process.stderr.write('[nf] WARNING: nf.json: context_monitor.warn_pct must be less than critical_pct; resetting to defaults\n');
232
283
  config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
233
284
  config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
234
285
  }
@@ -241,17 +292,113 @@ function validateConfig(config) {
241
292
  }
242
293
  }
243
294
 
295
+ // Validate budget sub-object
296
+ if (typeof config.budget !== 'object' || config.budget === null) {
297
+ process.stderr.write('[nf] WARNING: nf.json: budget must be an object; using defaults\n');
298
+ config.budget = { ...DEFAULT_CONFIG.budget };
299
+ } else {
300
+ // Validate session_limit_tokens: must be null, undefined, or integer >= 1000
301
+ if (config.budget.session_limit_tokens !== null && config.budget.session_limit_tokens !== undefined) {
302
+ if (!Number.isInteger(config.budget.session_limit_tokens) || config.budget.session_limit_tokens < 1000) {
303
+ process.stderr.write('[nf] WARNING: nf.json: budget.session_limit_tokens must be null or integer >= 1000; defaulting to null\n');
304
+ config.budget.session_limit_tokens = null;
305
+ }
306
+ }
307
+ // Validate warn_pct: integer 1-99, default 60
308
+ if (!Number.isInteger(config.budget.warn_pct) || config.budget.warn_pct < 1 || config.budget.warn_pct > 99) {
309
+ process.stderr.write('[nf] WARNING: nf.json: budget.warn_pct must be an integer 1-99; defaulting to 60\n');
310
+ config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
311
+ }
312
+ // Validate downgrade_pct: integer 1-100, default 85
313
+ if (!Number.isInteger(config.budget.downgrade_pct) || config.budget.downgrade_pct < 1 || config.budget.downgrade_pct > 100) {
314
+ process.stderr.write('[nf] WARNING: nf.json: budget.downgrade_pct must be an integer 1-100; defaulting to 85\n');
315
+ config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
316
+ }
317
+ // Validate warn_pct < downgrade_pct
318
+ if (config.budget.warn_pct >= config.budget.downgrade_pct) {
319
+ process.stderr.write('[nf] WARNING: nf.json: budget.warn_pct must be less than downgrade_pct; resetting to defaults\n');
320
+ config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
321
+ config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
322
+ }
323
+ // Fill missing sub-keys with defaults
324
+ if (config.budget.session_limit_tokens === undefined) {
325
+ config.budget.session_limit_tokens = DEFAULT_CONFIG.budget.session_limit_tokens;
326
+ }
327
+ if (config.budget.warn_pct === undefined) {
328
+ config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
329
+ }
330
+ if (config.budget.downgrade_pct === undefined) {
331
+ config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
332
+ }
333
+ }
334
+
335
+ // Validate stall_detection sub-object
336
+ if (typeof config.stall_detection !== 'object' || config.stall_detection === null) {
337
+ process.stderr.write('[nf] WARNING: nf.json: stall_detection must be an object; using defaults\n');
338
+ config.stall_detection = { ...DEFAULT_CONFIG.stall_detection };
339
+ } else {
340
+ // Validate timeout_s: positive integer, default 90
341
+ if (!Number.isInteger(config.stall_detection.timeout_s) || config.stall_detection.timeout_s < 1) {
342
+ process.stderr.write('[nf] WARNING: nf.json: stall_detection.timeout_s must be a positive integer; defaulting to 90\n');
343
+ config.stall_detection.timeout_s = DEFAULT_CONFIG.stall_detection.timeout_s;
344
+ }
345
+ // Validate consecutive_threshold: positive integer >= 1, default 2
346
+ if (!Number.isInteger(config.stall_detection.consecutive_threshold) || config.stall_detection.consecutive_threshold < 1) {
347
+ process.stderr.write('[nf] WARNING: nf.json: stall_detection.consecutive_threshold must be a positive integer; defaulting to 2\n');
348
+ config.stall_detection.consecutive_threshold = DEFAULT_CONFIG.stall_detection.consecutive_threshold;
349
+ }
350
+ // Validate check_commits: boolean, default true
351
+ if (typeof config.stall_detection.check_commits !== 'boolean') {
352
+ process.stderr.write('[nf] WARNING: nf.json: stall_detection.check_commits must be a boolean; defaulting to true\n');
353
+ config.stall_detection.check_commits = DEFAULT_CONFIG.stall_detection.check_commits;
354
+ }
355
+ // Fill missing sub-keys with defaults
356
+ if (config.stall_detection.timeout_s === undefined) {
357
+ config.stall_detection.timeout_s = DEFAULT_CONFIG.stall_detection.timeout_s;
358
+ }
359
+ if (config.stall_detection.consecutive_threshold === undefined) {
360
+ config.stall_detection.consecutive_threshold = DEFAULT_CONFIG.stall_detection.consecutive_threshold;
361
+ }
362
+ if (config.stall_detection.check_commits === undefined) {
363
+ config.stall_detection.check_commits = DEFAULT_CONFIG.stall_detection.check_commits;
364
+ }
365
+ }
366
+
367
+ // Validate smart_compact sub-object
368
+ if (typeof config.smart_compact !== 'object' || config.smart_compact === null) {
369
+ process.stderr.write('[nf] WARNING: nf.json: smart_compact must be an object; using defaults\n');
370
+ config.smart_compact = { ...DEFAULT_CONFIG.smart_compact };
371
+ } else {
372
+ // Validate enabled: boolean, default true
373
+ if (typeof config.smart_compact.enabled !== 'boolean') {
374
+ process.stderr.write('[nf] WARNING: nf.json: smart_compact.enabled must be a boolean; defaulting to true\n');
375
+ config.smart_compact.enabled = DEFAULT_CONFIG.smart_compact.enabled;
376
+ }
377
+ // Validate context_warn_pct: integer 1-99, default 60
378
+ if (!Number.isInteger(config.smart_compact.context_warn_pct) || config.smart_compact.context_warn_pct < 1 || config.smart_compact.context_warn_pct > 99) {
379
+ process.stderr.write('[nf] WARNING: nf.json: smart_compact.context_warn_pct must be an integer 1-99; defaulting to 60\n');
380
+ config.smart_compact.context_warn_pct = DEFAULT_CONFIG.smart_compact.context_warn_pct;
381
+ }
382
+ // Fill missing sub-keys with defaults
383
+ if (config.smart_compact.enabled === undefined) {
384
+ config.smart_compact.enabled = DEFAULT_CONFIG.smart_compact.enabled;
385
+ }
386
+ if (config.smart_compact.context_warn_pct === undefined) {
387
+ config.smart_compact.context_warn_pct = DEFAULT_CONFIG.smart_compact.context_warn_pct;
388
+ }
389
+ }
390
+
244
391
  // Validate model_tier_planner and model_tier_worker
245
392
  const VALID_TIERS = ['haiku', 'sonnet', 'opus'];
246
393
  if (config.model_tier_planner !== undefined) {
247
394
  if (typeof config.model_tier_planner !== 'string' || !VALID_TIERS.includes(config.model_tier_planner)) {
248
- process.stderr.write('[qgsd] WARNING: qgsd.json: model_tier_planner must be "haiku", "sonnet", or "opus"; removing\n');
395
+ process.stderr.write('[nf] WARNING: nf.json: model_tier_planner must be "haiku", "sonnet", or "opus"; removing\n');
249
396
  delete config.model_tier_planner;
250
397
  }
251
398
  }
252
399
  if (config.model_tier_worker !== undefined) {
253
400
  if (typeof config.model_tier_worker !== 'string' || !VALID_TIERS.includes(config.model_tier_worker)) {
254
- process.stderr.write('[qgsd] WARNING: qgsd.json: model_tier_worker must be "haiku", "sonnet", or "opus"; removing\n');
401
+ process.stderr.write('[nf] WARNING: nf.json: model_tier_worker must be "haiku", "sonnet", or "opus"; removing\n');
255
402
  delete config.model_tier_worker;
256
403
  }
257
404
  }
@@ -259,32 +406,41 @@ function validateConfig(config) {
259
406
  // Validate task_envelope_enabled
260
407
  if (config.task_envelope_enabled !== undefined) {
261
408
  if (typeof config.task_envelope_enabled !== 'boolean') {
262
- process.stderr.write('[qgsd] WARNING: qgsd.json: task_envelope_enabled must be a boolean; using default true\n');
409
+ process.stderr.write('[nf] WARNING: nf.json: task_envelope_enabled must be a boolean; using default true\n');
263
410
  config.task_envelope_enabled = true;
264
411
  }
265
412
  }
266
413
 
414
+ // Validate hook_profile
415
+ const VALID_PROFILES = ['minimal', 'standard', 'strict'];
416
+ if (config.hook_profile !== undefined) {
417
+ if (typeof config.hook_profile !== 'string' || !VALID_PROFILES.includes(config.hook_profile)) {
418
+ process.stderr.write('[nf] WARNING: nf.json: hook_profile must be "minimal", "standard", or "strict"; defaulting to "standard"\n');
419
+ config.hook_profile = 'standard';
420
+ }
421
+ }
422
+
267
423
  return config;
268
424
  }
269
425
 
270
- // Loads the two-layer QGSD config.
426
+ // Loads the two-layer nForma config.
271
427
  //
272
- // Layer 1 (global): ~/.claude/qgsd.json
273
- // Layer 2 (project): <projectDir>/.claude/qgsd.json (defaults to process.cwd())
428
+ // Layer 1 (global): ~/.claude/nf.json
429
+ // Layer 2 (project): <projectDir>/.claude/nf.json (defaults to process.cwd())
274
430
  //
275
431
  // Merge is shallow: { ...DEFAULT_CONFIG, ...global, ...project }
276
432
  // If both layers are missing/malformed, returns DEFAULT_CONFIG with a warning.
277
433
  // All warnings go to stderr — stdout is never touched.
278
434
  function loadConfig(projectDir) {
279
- const globalPath = path.join(os.homedir(), '.claude', 'qgsd.json');
280
- const projectPath = path.join(projectDir || process.cwd(), '.claude', 'qgsd.json');
435
+ const globalPath = path.join(os.homedir(), '.claude', 'nf.json');
436
+ const projectPath = path.join(projectDir || process.cwd(), '.claude', 'nf.json');
281
437
 
282
438
  const globalObj = readConfigFile(globalPath);
283
439
  const projectObj = readConfigFile(projectPath);
284
440
 
285
441
  let config;
286
442
  if (!globalObj && !projectObj) {
287
- process.stderr.write('[qgsd] WARNING: No qgsd.json found at ' + globalPath + ' or ' + projectPath + '; using hardcoded defaults\n');
443
+ process.stderr.write('[nf] WARNING: No nf.json found at ' + globalPath + ' or ' + projectPath + '; using hardcoded defaults\n');
288
444
  config = { ...DEFAULT_CONFIG };
289
445
  } else {
290
446
  config = { ...DEFAULT_CONFIG, ...(globalObj || {}), ...(projectObj || {}) };
@@ -294,4 +450,4 @@ function loadConfig(projectDir) {
294
450
  return config;
295
451
  }
296
452
 
297
- module.exports = { loadConfig, DEFAULT_CONFIG, SLOT_TOOL_SUFFIX, slotToToolCall };
453
+ module.exports = { loadConfig, validateConfig, DEFAULT_CONFIG, SLOT_TOOL_SUFFIX, slotToToolCall, shouldRunHook, HOOK_PROFILE_MAP };
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
  // bin/conformance-schema.cjs
3
3
  // Single source of truth for conformance event field enumerations.
4
- // Imported by hooks (qgsd-stop.js, qgsd-prompt.js, qgsd-circuit-breaker.js) and validate-traces.cjs.
4
+ // Imported by hooks (nf-stop.js, nf-prompt.js, nf-circuit-breaker.js) and validate-traces.cjs.
5
5
  // NEVER add external require() calls — hooks have zero runtime dependencies.
6
6
 
7
- const VALID_ACTIONS = ['quorum_start', 'quorum_complete', 'quorum_block', 'deliberation_round', 'circuit_break'];
7
+ const VALID_ACTIONS = ['quorum_start', 'quorum_complete', 'quorum_block', 'deliberation_round', 'circuit_break', 'cache_hit', 'budget_warn', 'budget_downgrade', 'stall_detected', 'security_sweep'];
8
8
  const VALID_PHASES = ['IDLE', 'COLLECTING_VOTES', 'DELIBERATING', 'DECIDED'];
9
9
  const VALID_OUTCOMES = ['APPROVE', 'BLOCK', 'UNAVAILABLE', 'DELIBERATE'];
10
10
  const schema_version = '1';
@@ -1,18 +1,67 @@
1
1
  #!/usr/bin/env node
2
2
  // hooks/gsd-context-monitor.js
3
- // PostToolUse hook — context window monitor.
3
+ // PostToolUse hook — context window monitor with budget tracking and smart compact.
4
4
  //
5
5
  // Reads context_window metrics from the PostToolUse event payload.
6
6
  // Injects WARNING or CRITICAL into additionalContext when context usage
7
- // exceeds configurable thresholds. Fails open on all errors.
7
+ // exceeds configurable thresholds. Also injects budget warnings and
8
+ // smart compact suggestions at clean workflow boundaries.
9
+ // Fails open on all errors.
8
10
  //
9
11
  // Config: context_monitor.warn_pct (default 70%) and
10
- // context_monitor.critical_pct (default 90%) in qgsd.json.
12
+ // context_monitor.critical_pct (default 90%) in nf.json.
11
13
  // Two-layer merge via shared config-loader.
12
14
 
13
15
  'use strict';
14
16
 
15
- const { loadConfig } = require('./config-loader');
17
+ const path = require('path');
18
+ const fs = require('fs');
19
+ const { loadConfig, shouldRunHook } = require('./config-loader');
20
+
21
+ // Append a conformance event to conformance-events.jsonl (fail-open)
22
+ function appendConformanceEvent(event) {
23
+ try {
24
+ let eventsPath;
25
+ try {
26
+ const planningPaths = require(path.join(__dirname, '..', 'bin', 'planning-paths.cjs'));
27
+ eventsPath = planningPaths.resolveWithFallback(process.cwd(), 'conformance-events');
28
+ } catch {
29
+ eventsPath = path.join(process.cwd(), '.planning', 'telemetry', 'conformance-events.jsonl');
30
+ }
31
+ const dir = path.dirname(eventsPath);
32
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
33
+ fs.appendFileSync(eventsPath, JSON.stringify(event) + '\n', 'utf8');
34
+ } catch {
35
+ // Fail-open
36
+ }
37
+ }
38
+
39
+ // Detect if the just-completed tool call represents a clean workflow boundary
40
+ function detectCleanBoundary(toolName, toolInput) {
41
+ if (toolName !== 'Bash' || !toolInput) return null;
42
+ const input = typeof toolInput === 'string' ? toolInput : (toolInput.command || '');
43
+ if (input.includes('gsd-tools.cjs phase-complete')) return 'phase_complete';
44
+ if (input.includes('gsd-tools.cjs commit') && input.includes('VERIFICATION')) return 'verification_done';
45
+ if (input.includes('gsd-tools.cjs commit')) return 'commit';
46
+ return null;
47
+ }
48
+
49
+ // Format a smart compact suggestion
50
+ function formatCompactSuggestion(usedPct, boundaryType) {
51
+ return `SMART COMPACT SUGGESTION: Context at ${usedPct}% -- clean workflow boundary (${boundaryType}).
52
+ Consider running /compact now.
53
+
54
+ What survives compaction:
55
+ + STATE.md Current Position (phase, plan, last activity)
56
+ + Pending task files (.claude/pending-task*.txt)
57
+ + CLAUDE.md project rules
58
+
59
+ What will be lost:
60
+ - Conversation history and reasoning
61
+ - File contents read during this session
62
+ - Quorum deliberation transcripts
63
+ - Intermediate tool outputs`;
64
+ }
16
65
 
17
66
  let raw = '';
18
67
  process.stdin.setEncoding('utf8');
@@ -21,6 +70,13 @@ process.stdin.on('end', () => {
21
70
  try {
22
71
  const input = JSON.parse(raw);
23
72
 
73
+ // Profile guard — exit early if this hook is not active for the current profile
74
+ const config = loadConfig(input.cwd || process.cwd());
75
+ const profile = config.hook_profile || 'standard';
76
+ if (!shouldRunHook('gsd-context-monitor', profile)) {
77
+ process.exit(0);
78
+ }
79
+
24
80
  const ctxWindow = input.context_window;
25
81
  if (!ctxWindow || ctxWindow.remaining_percentage == null) {
26
82
  process.exit(0); // No context data — fail-open
@@ -29,30 +85,79 @@ process.stdin.on('end', () => {
29
85
  const remaining = ctxWindow.remaining_percentage;
30
86
  const usedPct = Math.round(100 - remaining);
31
87
 
32
- const config = loadConfig(input.cwd || process.cwd());
33
88
  const monitorCfg = config.context_monitor || {};
34
89
  const warnPct = monitorCfg.warn_pct != null ? monitorCfg.warn_pct : 70;
35
90
  const criticalPct = monitorCfg.critical_pct != null ? monitorCfg.critical_pct : 90;
36
91
 
37
- let message;
92
+ // Context window message
93
+ let contextMessage = null;
38
94
  if (usedPct >= criticalPct) {
39
- message =
95
+ contextMessage =
40
96
  `CONTEXT MONITOR CRITICAL: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
41
97
  'STOP new work immediately. Save state and inform the user that context is nearly exhausted. ' +
42
- 'If using QGSD, run /qgsd:pause-work to save execution state.';
98
+ 'Run /nf:pause-work to save execution state.';
43
99
  } else if (usedPct >= warnPct) {
44
- message =
100
+ contextMessage =
45
101
  `CONTEXT MONITOR WARNING: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
46
102
  'Begin wrapping up current task. Do not start new complex work. ' +
47
- 'If using QGSD, consider /qgsd:pause-work to save state.';
48
- } else {
49
- process.exit(0); // Below warning threshold — no injection needed
103
+ 'Consider /nf:pause-work to save state.';
104
+ }
105
+
106
+ // Budget tracking
107
+ let budgetMessage = null;
108
+ const budgetTracker = (() => {
109
+ try { return require(path.join(__dirname, '..', 'bin', 'budget-tracker.cjs')); }
110
+ catch { return null; }
111
+ })();
112
+
113
+ if (budgetTracker) {
114
+ const status = budgetTracker.computeBudgetStatus(usedPct, config.budget || {}, config.agent_config || {});
115
+ if (status.active && status.shouldDowngrade) {
116
+ const downgradeResult = budgetTracker.triggerProfileDowngrade(input.cwd || process.cwd());
117
+ budgetMessage = budgetTracker.formatBudgetWarning(status, downgradeResult);
118
+ appendConformanceEvent({
119
+ action: 'budget_downgrade',
120
+ ts: new Date().toISOString(),
121
+ budget_used_pct: status.budgetUsedPct,
122
+ estimated_tokens: status.estimatedTokens,
123
+ downgrade: downgradeResult,
124
+ });
125
+ } else if (status.active && status.shouldWarn) {
126
+ budgetMessage = budgetTracker.formatBudgetWarning(status, null);
127
+ appendConformanceEvent({
128
+ action: 'budget_warn',
129
+ ts: new Date().toISOString(),
130
+ budget_used_pct: status.budgetUsedPct,
131
+ estimated_tokens: status.estimatedTokens,
132
+ });
133
+ }
50
134
  }
51
135
 
136
+ // Smart compact suggestion
137
+ let compactMessage = null;
138
+ const smartCfg = config.smart_compact || {};
139
+ if (smartCfg.enabled !== false) {
140
+ const compactThreshold = smartCfg.context_warn_pct || 60;
141
+ if (usedPct >= compactThreshold) {
142
+ const boundary = detectCleanBoundary(input.tool_name, input.tool_input);
143
+ if (boundary) {
144
+ compactMessage = formatCompactSuggestion(usedPct, boundary);
145
+ }
146
+ }
147
+ }
148
+
149
+ // Combine all messages
150
+ const messages = [contextMessage, budgetMessage, compactMessage].filter(Boolean);
151
+ if (messages.length === 0) {
152
+ process.exit(0);
153
+ }
154
+
155
+ const combined = messages.join('\n\n');
156
+
52
157
  process.stdout.write(JSON.stringify({
53
158
  hookSpecificOutput: {
54
159
  hookEventName: 'PostToolUse',
55
- additionalContext: message,
160
+ additionalContext: combined,
56
161
  },
57
162
  }));
58
163
  process.exit(0);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Check for QGSD updates in background, write result to cache
2
+ // Check for nForma updates in background, write result to cache
3
3
  // Called by SessionStart hook - runs once per session
4
4
 
5
5
  const fs = require('fs');
@@ -10,11 +10,11 @@ const { spawn } = require('child_process');
10
10
  const homeDir = os.homedir();
11
11
  const cwd = process.cwd();
12
12
  const cacheDir = path.join(homeDir, '.claude', 'cache');
13
- const cacheFile = path.join(cacheDir, 'qgsd-update-check.json');
13
+ const cacheFile = path.join(cacheDir, 'nf-update-check.json');
14
14
 
15
15
  // VERSION file locations (check project first, then global)
16
- const projectVersionFile = path.join(cwd, '.claude', 'qgsd', 'VERSION');
17
- const globalVersionFile = path.join(homeDir, '.claude', 'qgsd', 'VERSION');
16
+ const projectVersionFile = path.join(cwd, '.claude', 'nf', 'VERSION');
17
+ const globalVersionFile = path.join(homeDir, '.claude', 'nf', 'VERSION');
18
18
 
19
19
  // Ensure cache directory exists
20
20
  if (!fs.existsSync(cacheDir)) {
@@ -42,7 +42,7 @@ const child = spawn(process.execPath, ['-e', `
42
42
 
43
43
  let latest = null;
44
44
  try {
45
- latest = execSync('npm view @nforma.ai/qgsd version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
45
+ latest = execSync('npm view @nforma.ai/nforma version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
46
46
  } catch (e) {}
47
47
 
48
48
  const result = {