@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
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-token-collector.js
2
+ // hooks/nf-token-collector.js
3
3
  // SubagentStop hook — reads agent_transcript_path, sums message.usage fields,
4
4
  // appends a token record to .planning/token-usage.jsonl.
5
5
  //
6
6
  // Guards:
7
- // - Only processes agent_type === 'qgsd-quorum-slot-worker' (exits 0 otherwise)
7
+ // - Only processes agent_type === 'nf-quorum-slot-worker' (exits 0 otherwise)
8
8
  // - If transcript path is absent or missing: writes null-token record and exits 0 (fail-open)
9
9
  // - isSidechain === true entries are excluded from token sum
10
10
  // - isApiErrorMessage === true entries are excluded from token sum
@@ -15,6 +15,7 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
+ const { loadConfig, shouldRunHook } = require('./config-loader');
18
19
 
19
20
  // Resolve slot name from correlation file or last_assistant_message preamble.
20
21
  // Order:
@@ -78,8 +79,15 @@ function main() {
78
79
  try {
79
80
  const input = JSON.parse(raw);
80
81
 
81
- // Guard: only process qgsd-quorum-slot-worker subagents
82
- if (input.agent_type !== 'qgsd-quorum-slot-worker') {
82
+ // Profile guard exit early if this hook is not active for the current profile
83
+ const config = loadConfig();
84
+ const profile = config.hook_profile || 'standard';
85
+ if (!shouldRunHook('nf-token-collector', profile)) {
86
+ process.exit(0);
87
+ }
88
+
89
+ // Guard: only process nf-quorum-slot-worker subagents
90
+ if (input.agent_type !== 'nf-quorum-slot-worker') {
83
91
  process.exit(0);
84
92
  }
85
93
 
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ // Test suite for hooks/nf-token-collector.js
3
+ // Uses Node.js built-in test runner: node --test hooks/nf-token-collector.test.js
4
+ //
5
+ // Each test spawns the hook as a child process with a mock stdin JSON payload,
6
+ // writes fixture JSONL transcript to an isolated tmpdir, asserts stdout/exitCode/file output.
7
+
8
+ 'use strict';
9
+
10
+ const { test } = require('node:test');
11
+ const assert = require('node:assert/strict');
12
+ const { spawnSync } = require('child_process');
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+
17
+ const HOOK_PATH = path.join(__dirname, 'nf-token-collector.js');
18
+
19
+ // Helper: create isolated tmpdir per test
20
+ function makeTmpDir() {
21
+ return path.join(os.tmpdir(), 'nf-tc-' + Date.now() + '-' + Math.random().toString(36).slice(2));
22
+ }
23
+
24
+ // Helper: run the hook with a given stdin JSON payload, using tmpDir as cwd
25
+ function runHook(stdinPayload, tmpDir) {
26
+ fs.mkdirSync(tmpDir, { recursive: true });
27
+ const result = spawnSync('node', [HOOK_PATH], {
28
+ input: JSON.stringify(stdinPayload),
29
+ cwd: tmpDir,
30
+ encoding: 'utf8',
31
+ timeout: 5000,
32
+ });
33
+ return {
34
+ stdout: result.stdout || '',
35
+ stderr: result.stderr || '',
36
+ exitCode: result.status,
37
+ };
38
+ }
39
+
40
+ // Helper: read token-usage.jsonl from tmpDir
41
+ function readTokenLog(tmpDir) {
42
+ const logPath = path.join(tmpDir, '.planning', 'telemetry', 'token-usage.jsonl');
43
+ if (!fs.existsSync(logPath)) return null;
44
+ const lines = fs.readFileSync(logPath, 'utf8').split('\n').filter(l => l.trim());
45
+ return lines.map(l => JSON.parse(l));
46
+ }
47
+
48
+ // Helper: write a fixture transcript to tmpDir
49
+ function writeTranscript(tmpDir, entries) {
50
+ fs.mkdirSync(tmpDir, { recursive: true });
51
+ const transcriptPath = path.join(tmpDir, 'transcript.jsonl');
52
+ const content = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
53
+ fs.writeFileSync(transcriptPath, content, 'utf8');
54
+ return transcriptPath;
55
+ }
56
+
57
+ test('normal case: appends correct record to token-usage.jsonl', () => {
58
+ const tmpDir = makeTmpDir();
59
+ const transcriptPath = writeTranscript(tmpDir, [
60
+ {
61
+ type: 'assistant',
62
+ message: {
63
+ usage: {
64
+ input_tokens: 100,
65
+ output_tokens: 25,
66
+ cache_creation_input_tokens: 5000,
67
+ cache_read_input_tokens: 200,
68
+ },
69
+ },
70
+ isSidechain: false,
71
+ isApiErrorMessage: false,
72
+ },
73
+ ]);
74
+
75
+ const payload = {
76
+ agent_type: 'nf-quorum-slot-worker',
77
+ session_id: 'sess1',
78
+ agent_id: 'agent1',
79
+ agent_transcript_path: transcriptPath,
80
+ last_assistant_message: 'slot: claude-1\nvote: APPROVE\nrationale: looks good',
81
+ };
82
+
83
+ const { exitCode } = runHook(payload, tmpDir);
84
+ assert.equal(exitCode, 0);
85
+
86
+ const records = readTokenLog(tmpDir);
87
+ assert.ok(records && records.length === 1, 'Expected exactly 1 record');
88
+ assert.equal(records[0].slot, 'claude-1');
89
+ assert.equal(records[0].input_tokens, 100);
90
+ assert.equal(records[0].output_tokens, 25);
91
+ assert.equal(records[0].cache_creation_input_tokens, 5000);
92
+ assert.equal(records[0].cache_read_input_tokens, 200);
93
+ assert.equal(records[0].session_id, 'sess1');
94
+ assert.equal(records[0].agent_id, 'agent1');
95
+ });
96
+
97
+ test('isSidechain entries are excluded', () => {
98
+ const tmpDir = makeTmpDir();
99
+ const transcriptPath = writeTranscript(tmpDir, [
100
+ {
101
+ type: 'assistant',
102
+ message: { usage: { input_tokens: 999, output_tokens: 999 } },
103
+ isSidechain: true,
104
+ isApiErrorMessage: false,
105
+ },
106
+ {
107
+ type: 'assistant',
108
+ message: { usage: { input_tokens: 50, output_tokens: 10 } },
109
+ isSidechain: false,
110
+ isApiErrorMessage: false,
111
+ },
112
+ ]);
113
+
114
+ const payload = {
115
+ agent_type: 'nf-quorum-slot-worker',
116
+ session_id: 's1',
117
+ agent_id: 'a1',
118
+ agent_transcript_path: transcriptPath,
119
+ last_assistant_message: 'slot: claude-1\nvote: APPROVE',
120
+ };
121
+
122
+ const { exitCode } = runHook(payload, tmpDir);
123
+ assert.equal(exitCode, 0);
124
+
125
+ const records = readTokenLog(tmpDir);
126
+ assert.ok(records && records.length === 1);
127
+ // Only the non-sidechain entry counts: 50 input, 10 output
128
+ assert.equal(records[0].input_tokens, 50);
129
+ assert.equal(records[0].output_tokens, 10);
130
+ });
131
+
132
+ test('isApiErrorMessage entries are excluded', () => {
133
+ const tmpDir = makeTmpDir();
134
+ const transcriptPath = writeTranscript(tmpDir, [
135
+ {
136
+ type: 'assistant',
137
+ message: { usage: { input_tokens: 500, output_tokens: 100 } },
138
+ isSidechain: false,
139
+ isApiErrorMessage: true,
140
+ },
141
+ ]);
142
+
143
+ const payload = {
144
+ agent_type: 'nf-quorum-slot-worker',
145
+ session_id: 's1',
146
+ agent_id: 'a1',
147
+ agent_transcript_path: transcriptPath,
148
+ last_assistant_message: 'slot: claude-2\nvote: APPROVE',
149
+ };
150
+
151
+ const { exitCode } = runHook(payload, tmpDir);
152
+ assert.equal(exitCode, 0);
153
+
154
+ const records = readTokenLog(tmpDir);
155
+ assert.ok(records && records.length === 1);
156
+ // Error entries excluded → 0 tokens summed
157
+ assert.equal(records[0].input_tokens, 0);
158
+ assert.equal(records[0].output_tokens, 0);
159
+ });
160
+
161
+ test('null transcript path: exits 0 and writes null record', () => {
162
+ const tmpDir = makeTmpDir();
163
+
164
+ const payload = {
165
+ agent_type: 'nf-quorum-slot-worker',
166
+ session_id: 's1',
167
+ agent_id: 'a1',
168
+ agent_transcript_path: null,
169
+ last_assistant_message: 'slot: claude-1\nvote: APPROVE',
170
+ };
171
+
172
+ const { exitCode } = runHook(payload, tmpDir);
173
+ assert.equal(exitCode, 0);
174
+
175
+ const records = readTokenLog(tmpDir);
176
+ assert.ok(records && records.length === 1, 'Expected 1 null record');
177
+ assert.equal(records[0].input_tokens, null);
178
+ assert.equal(records[0].output_tokens, null);
179
+ });
180
+
181
+ test('non-nf agent type: exits 0, no file written', () => {
182
+ const tmpDir = makeTmpDir();
183
+
184
+ const payload = {
185
+ agent_type: 'other',
186
+ session_id: 's1',
187
+ agent_id: 'a1',
188
+ agent_transcript_path: null,
189
+ last_assistant_message: 'hello',
190
+ };
191
+
192
+ const { exitCode } = runHook(payload, tmpDir);
193
+ assert.equal(exitCode, 0);
194
+
195
+ const records = readTokenLog(tmpDir);
196
+ assert.equal(records, null, 'No file should be written for non-nf agents');
197
+ });
198
+
199
+ test('slot resolution: fallback to last_assistant_message when no correlation file', () => {
200
+ const tmpDir = makeTmpDir();
201
+ const transcriptPath = writeTranscript(tmpDir, [
202
+ {
203
+ type: 'assistant',
204
+ message: { usage: { input_tokens: 75, output_tokens: 15 } },
205
+ isSidechain: false,
206
+ isApiErrorMessage: false,
207
+ },
208
+ ]);
209
+
210
+ const payload = {
211
+ agent_type: 'nf-quorum-slot-worker',
212
+ session_id: 's1',
213
+ agent_id: 'a2',
214
+ agent_transcript_path: transcriptPath,
215
+ last_assistant_message: 'slot: gemini-1\nvote: APPROVE\nrationale: good',
216
+ };
217
+
218
+ const { exitCode } = runHook(payload, tmpDir);
219
+ assert.equal(exitCode, 0);
220
+
221
+ const records = readTokenLog(tmpDir);
222
+ assert.ok(records && records.length === 1);
223
+ assert.equal(records[0].slot, 'gemini-1');
224
+ });
225
+
226
+ test('slot resolution: correlation file exists with slot: null, falls back to last_assistant_message', () => {
227
+ const tmpDir = makeTmpDir();
228
+ const planningDir = path.join(tmpDir, '.planning');
229
+ fs.mkdirSync(planningDir, { recursive: true });
230
+
231
+ // Write a correlation file with slot: null
232
+ const corrFile = path.join(planningDir, 'quorum-slot-corr-a1.json');
233
+ fs.writeFileSync(corrFile, JSON.stringify({ agent_id: 'a1', slot: null, ts: new Date().toISOString() }), 'utf8');
234
+
235
+ const transcriptPath = writeTranscript(tmpDir, [
236
+ {
237
+ type: 'assistant',
238
+ message: { usage: { input_tokens: 200, output_tokens: 40 } },
239
+ isSidechain: false,
240
+ isApiErrorMessage: false,
241
+ },
242
+ ]);
243
+
244
+ const payload = {
245
+ agent_type: 'nf-quorum-slot-worker',
246
+ session_id: 's1',
247
+ agent_id: 'a1',
248
+ agent_transcript_path: transcriptPath,
249
+ last_assistant_message: 'slot: claude-2\nvote: APPROVE',
250
+ };
251
+
252
+ const { exitCode } = runHook(payload, tmpDir);
253
+ assert.equal(exitCode, 0);
254
+
255
+ // Correlation file should be deleted after reading
256
+ assert.equal(fs.existsSync(corrFile), false, 'Correlation file should be deleted after reading');
257
+
258
+ const records = readTokenLog(tmpDir);
259
+ assert.ok(records && records.length === 1);
260
+ // slot: null in corr file → fallback to last_assistant_message → claude-2
261
+ assert.equal(records[0].slot, 'claude-2');
262
+ });
@@ -104,7 +104,7 @@ function buildSlotTools(provider) {
104
104
  // Universal: identity
105
105
  tools.push({
106
106
  name: 'identity',
107
- description: 'Get server identity: name, version, active LLM model, and MCP server name. Used by QGSD to fingerprint the active quorum team.',
107
+ description: 'Get server identity: name, version, active LLM model, and MCP server name. Used by nForma to fingerprint the active quorum team.',
108
108
  inputSchema: NO_ARGS_SCHEMA,
109
109
  });
110
110
 
@@ -501,7 +501,7 @@ function buildIdentityResult(provider) {
501
501
  model,
502
502
  display_provider: provider.display_provider ?? null,
503
503
  provider: provider.description,
504
- install_method: 'qgsd-monorepo',
504
+ install_method: 'nf-monorepo',
505
505
  });
506
506
  }
507
507
 
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@nforma.ai/nforma",
3
- "version": "0.2.1",
3
+ "version": "0.28.0",
4
4
  "description": "nForma — Quorum Gets Shit Done. Multi-model quorum enforcement for GSD planning commands via Claude Code hooks.",
5
5
  "bin": {
6
6
  "nforma": "bin/install.js",
7
- "qgsd": "bin/install.js",
8
7
  "get-shit-done-cc": "bin/install.js"
9
8
  },
10
9
  "files": [
@@ -70,7 +69,7 @@
70
69
  "scripts": {
71
70
  "postinstall": "node scripts/postinstall.js",
72
71
  "build:hooks": "node scripts/build-hooks.js",
73
- "build:machines": "npx tsup src/machines/qgsd-workflow.machine.ts --format cjs --out-dir dist/machines --no-splitting --tsconfig tsconfig.formal.json",
72
+ "build:machines": "npx tsup src/machines/nf-workflow.machine.ts --format cjs --out-dir dist/machines --no-splitting --tsconfig tsconfig.formal.json",
74
73
  "generate-terminal": "node scripts/generate-terminal-svg.js",
75
74
  "generate-logo": "node scripts/generate-logo-svg.js",
76
75
  "generate-assets": "npm run generate-terminal && npm run generate-logo",
@@ -88,7 +87,8 @@
88
87
  "secrets:audit": "detect-secrets audit .secrets.baseline",
89
88
  "secrets:history": "bash scripts/secret-audit.sh",
90
89
  "test": "npm run test:ci && npm run test:formal",
91
- "test:ci": "node scripts/lint-isolation.js && node --test hooks/qgsd-precompact.test.js hooks/gsd-context-monitor.test.js hooks/qgsd-session-start.test.js bin/conformance-schema.test.cjs bin/resolve-cli.test.cjs bin/secrets.test.cjs bin/verify-quorum-health.test.cjs hooks/qgsd-stop.test.js hooks/config-loader.test.js qgsd-core/bin/gsd-tools.test.cjs hooks/qgsd-circuit-breaker.test.js hooks/qgsd-prompt.test.js bin/update-scoreboard.test.cjs hooks/qgsd-statusline.test.js bin/review-mcp-logs.test.cjs bin/migrate-to-slots.test.cjs bin/validate-traces.test.cjs bin/write-check-result.test.cjs bin/check-results-exit.test.cjs bin/check-trace-redaction.test.cjs bin/check-trace-schema-drift.test.cjs bin/qgsd.test.cjs bin/set-secret.test.cjs bin/issue-classifier.test.cjs bin/generate-tla-cfg.test.cjs bin/ccr-secure-config.test.cjs bin/gsd-quorum-slot-worker-improvements.test.cjs bin/quorum-improvements-signal.test.cjs bin/claude-md-references.test.cjs hooks/qgsd-spec-regen.test.js bin/propose-debug-invariants.test.cjs bin/aggregate-requirements.test.cjs bin/validate-requirements-haiku.test.cjs bin/call-quorum-slot-retry.test.cjs bin/provider-mapping.test.cjs",
90
+ "test:changed": "node bin/test-changed.cjs",
91
+ "test:ci": "node scripts/lint-isolation.js && node scripts/verify-hooks-sync.cjs && node --test hooks/nf-precompact.test.js hooks/gsd-context-monitor.test.js hooks/nf-session-start.test.js bin/conformance-schema.test.cjs bin/resolve-cli.test.cjs bin/secrets.test.cjs bin/verify-quorum-health.test.cjs hooks/nf-stop.test.js hooks/config-loader.test.js core/bin/gsd-tools.test.cjs hooks/nf-circuit-breaker.test.js hooks/nf-prompt.test.js bin/update-scoreboard.test.cjs hooks/nf-statusline.test.js bin/review-mcp-logs.test.cjs bin/migrate-to-slots.test.cjs bin/validate-traces.test.cjs bin/write-check-result.test.cjs bin/check-results-exit.test.cjs bin/check-trace-redaction.test.cjs bin/check-trace-schema-drift.test.cjs bin/nf.test.cjs bin/set-secret.test.cjs bin/issue-classifier.test.cjs bin/generate-tla-cfg.test.cjs bin/ccr-secure-config.test.cjs bin/gsd-quorum-slot-worker-improvements.test.cjs bin/quorum-improvements-signal.test.cjs bin/claude-md-references.test.cjs hooks/nf-spec-regen.test.js bin/propose-debug-invariants.test.cjs bin/aggregate-requirements.test.cjs bin/validate-requirements-haiku.test.cjs bin/call-quorum-slot-retry.test.cjs bin/provider-mapping.test.cjs",
92
92
  "test:formal": "node --test bin/run-tlc.test.cjs bin/run-alloy.test.cjs bin/export-prism-constants.test.cjs bin/generate-petri-net.test.cjs bin/run-breaker-tlc.test.cjs bin/run-oscillation-tlc.test.cjs bin/run-protocol-tlc.test.cjs bin/run-audit-alloy.test.cjs bin/run-transcript-alloy.test.cjs bin/run-installer-alloy.test.cjs bin/run-formal-verify.test.cjs bin/xstate-to-tla.test.cjs bin/run-account-manager-tlc.test.cjs bin/run-account-pool-alloy.test.cjs bin/run-oauth-rotation-prism.test.cjs bin/run-prism.test.cjs bin/check-spec-sync.test.cjs bin/sensitivity-sweep-feedback.test.cjs bin/roadmapper-formal-integration.test.cjs bin/test-formal-integration.test.cjs test/alloy-headless.test.cjs",
93
93
  "prepare": "husky"
94
94
  }
@@ -11,12 +11,19 @@ const DIST_DIR = path.join(HOOKS_DIR, 'dist');
11
11
 
12
12
  // Hooks to copy (pure Node.js, no bundling needed)
13
13
  const HOOKS_TO_COPY = [
14
- 'qgsd-check-update.js',
15
- 'qgsd-statusline.js',
16
- 'qgsd-prompt.js', // QGSD: UserPromptSubmit quorum injection hook
17
- 'qgsd-stop.js', // QGSD: Stop quorum verification gate
18
- 'config-loader.js', // QGSD: shared config loader (required by qgsd-prompt.js and qgsd-stop.js)
19
- 'qgsd-circuit-breaker.js', // QGSD: PreToolUse oscillation detection and state persistence
14
+ 'nf-check-update.js',
15
+ 'nf-statusline.js',
16
+ 'nf-prompt.js', // nForma: UserPromptSubmit quorum injection hook
17
+ 'nf-stop.js', // nForma: Stop quorum verification gate
18
+ 'nf-circuit-breaker.js', // nForma: PreToolUse oscillation detection and state persistence
19
+ 'nf-session-start.js', // nForma: SessionStart hook
20
+ 'nf-precompact.js', // nForma: PreCompact hook
21
+ 'nf-spec-regen.js', // nForma: PostToolUse spec regeneration
22
+ 'nf-token-collector.js', // nForma: SubagentStop token collection
23
+ 'nf-slot-correlator.js', // nForma: SubagentStart slot correlation
24
+ 'gsd-context-monitor.js', // nForma: PostToolUse context monitoring
25
+ 'config-loader.js', // shared config loader (required by multiple hooks)
26
+ 'conformance-schema.cjs', // shared conformance schema (required by nf-prompt, nf-stop, nf-circuit-breaker)
20
27
  ];
21
28
 
22
29
  function build() {
@@ -5,7 +5,7 @@
5
5
 
6
6
  set -euo pipefail
7
7
 
8
- echo "=== QGSD Full-History Secret Audit ==="
8
+ echo "=== nForma Full-History Secret Audit ==="
9
9
  echo ""
10
10
 
11
11
  # --- Gitleaks full-history scan ---
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CI guard: verifies that every hook registered by the installer has a
4
+ * corresponding entry in the build-hooks HOOKS_TO_COPY list, and that
5
+ * every require('./...') dependency inside those hooks is also included.
6
+ *
7
+ * Exits non-zero on drift so the test suite catches it early.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ROOT = path.resolve(__dirname, '..');
16
+ const INSTALL_JS = path.join(ROOT, 'bin', 'install.js');
17
+ const BUILD_HOOKS_JS = path.join(ROOT, 'scripts', 'build-hooks.js');
18
+ const HOOKS_DIR = path.join(ROOT, 'hooks');
19
+
20
+ // --- Extract HOOKS_TO_COPY from build-hooks.js ---
21
+ function getHooksToCopy() {
22
+ const src = fs.readFileSync(BUILD_HOOKS_JS, 'utf8');
23
+ const match = src.match(/HOOKS_TO_COPY\s*=\s*\[([\s\S]*?)\]/);
24
+ if (!match) throw new Error('Could not parse HOOKS_TO_COPY from build-hooks.js');
25
+ const entries = [];
26
+ for (const m of match[1].matchAll(/'([^']+)'/g)) {
27
+ entries.push(m[1]);
28
+ }
29
+ return new Set(entries);
30
+ }
31
+
32
+ // --- Extract hook filenames registered by the installer via buildHookCommand() ---
33
+ function getInstallerHooks() {
34
+ const src = fs.readFileSync(INSTALL_JS, 'utf8');
35
+ const hooks = new Set();
36
+ for (const m of src.matchAll(/buildHookCommand\(\s*\w+\s*,\s*'([^']+)'\s*\)/g)) {
37
+ hooks.add(m[1]);
38
+ }
39
+ return hooks;
40
+ }
41
+
42
+ // --- Extract local require('./...') dependencies from a hook source file ---
43
+ function getLocalRequires(filePath) {
44
+ const src = fs.readFileSync(filePath, 'utf8');
45
+ const deps = new Set();
46
+ for (const m of src.matchAll(/require\(\s*'\.\/([^']+)'\s*\)/g)) {
47
+ let dep = m[1];
48
+ // Node resolves require('./config-loader') to './config-loader.js'
49
+ // Normalize to match HOOKS_TO_COPY entries which include extensions
50
+ if (!path.extname(dep)) dep += '.js';
51
+ deps.add(dep);
52
+ }
53
+ return deps;
54
+ }
55
+
56
+ // --- Main ---
57
+ const hooksToCopy = getHooksToCopy();
58
+ const installerHooks = getInstallerHooks();
59
+ const errors = [];
60
+
61
+ // 1. Every hook registered by the installer must be in HOOKS_TO_COPY
62
+ for (const hook of installerHooks) {
63
+ if (!hooksToCopy.has(hook)) {
64
+ errors.push(`MISSING from HOOKS_TO_COPY: '${hook}' (registered in installer via buildHookCommand)`);
65
+ }
66
+ }
67
+
68
+ // 2. Every local require() dependency of copied hooks must also be in HOOKS_TO_COPY
69
+ for (const hook of hooksToCopy) {
70
+ const hookPath = path.join(HOOKS_DIR, hook);
71
+ if (!fs.existsSync(hookPath)) {
72
+ errors.push(`HOOKS_TO_COPY entry '${hook}' does not exist at ${hookPath}`);
73
+ continue;
74
+ }
75
+ const deps = getLocalRequires(hookPath);
76
+ for (const dep of deps) {
77
+ if (!hooksToCopy.has(dep)) {
78
+ errors.push(`MISSING from HOOKS_TO_COPY: '${dep}' (required by ${hook})`);
79
+ }
80
+ }
81
+ }
82
+
83
+ if (errors.length > 0) {
84
+ console.error('hooks-sync verification FAILED:\n');
85
+ for (const e of errors) console.error(` - ${e}`);
86
+ console.error('\nFix: update HOOKS_TO_COPY in scripts/build-hooks.js');
87
+ process.exit(1);
88
+ } else {
89
+ console.log(`hooks-sync OK: ${hooksToCopy.size} hooks in build list, ${installerHooks.size} registered by installer`);
90
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": [
3
- "QGSD Quorum Config — generated by QGSD installer. Safe to edit.",
3
+ "nForma Quorum Config — generated by nForma installer. Safe to edit.",
4
4
  "",
5
5
  "quorum_commands: GSD planning commands that require quorum before Claude delivers output.",
6
6
  " Add or remove command names to control which /gsd: commands are gated.",
@@ -14,11 +14,11 @@
14
14
  "fail_mode: 'open' (default) — unavailable models are skipped, quorum passes with available models.",
15
15
  " 'closed' is reserved for future use.",
16
16
  "",
17
- "Config layering: ~/.claude/qgsd.json (global) is loaded first.",
18
- " .claude/qgsd.json in the project directory overrides global values (project takes precedence).",
17
+ "Config layering: ~/.claude/nf.json (global) is loaded first.",
18
+ " .claude/nf.json in the project directory overrides global values (project takes precedence).",
19
19
  " Merge is SHALLOW: if a project config sets required_models, it entirely replaces global required_models.",
20
20
  " To add a model to the global list, copy the full required_models from global and add your entry.",
21
- " Create .claude/qgsd.json in a project to restrict quorum to fewer models or different commands.",
21
+ " Create .claude/nf.json in a project to restrict quorum to fewer models or different commands.",
22
22
  "",
23
23
  "circuit_breaker config uses the SAME shallow merge: a project config with only oscillation_depth set",
24
24
  " entirely replaces the global circuit_breaker object. commit_window falls back to DEFAULT (6), NOT the global value.",
@@ -1,18 +0,0 @@
1
- ---
2
- name: qgsd:join-discord
3
- description: Join the QGSD Discord community
4
- ---
5
-
6
- <objective>
7
- Display the Discord invite link for the QGSD community server.
8
- </objective>
9
-
10
- <output>
11
- # Join the QGSD Discord
12
-
13
- Connect with other QGSD users, get help, share what you're building, and stay updated.
14
-
15
- **Server link:** https://discord.com/servers/1474810068636663886
16
-
17
- Click the link or paste it into your browser to join.
18
- </output>
@@ -1,122 +0,0 @@
1
- #!/usr/bin/env node
2
- // hooks/qgsd-session-start.js
3
- // SessionStart hook — syncs QGSD keychain secrets into ~/.claude.json
4
- // on every session start so mcpServers env blocks always reflect current keychain state.
5
- //
6
- // Runs synchronously (hook expects process to exit) — uses async IIFE with catch.
7
-
8
- 'use strict';
9
-
10
- const path = require('path');
11
- const os = require('os');
12
- const fs = require('fs');
13
-
14
- // ─── Stdin accumulation (for hook input JSON containing cwd) ─────────────────
15
- let _stdinRaw = '';
16
- process.stdin.setEncoding('utf8');
17
- process.stdin.on('data', c => _stdinRaw += c);
18
-
19
- let _stdinReady;
20
- const _stdinPromise = new Promise(resolve => { _stdinReady = resolve; });
21
- process.stdin.on('end', () => _stdinReady());
22
-
23
- // Locate secrets.cjs — try installed global path first, then local dev path.
24
- //
25
- // IMPORTANT: install.js copies bin/*.cjs to ~/.claude/qgsd-bin/ (not ~/.claude/qgsd/bin/).
26
- // See bin/install.js line ~1679: binDest = path.join(targetDir, 'qgsd-bin')
27
- // where targetDir = os.homedir() + '/.claude'.
28
- function findSecrets() {
29
- const candidates = [
30
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'secrets.cjs'), // installed path
31
- path.join(__dirname, '..', 'bin', 'secrets.cjs'), // local dev path
32
- ];
33
- for (const p of candidates) {
34
- try {
35
- return require(p);
36
- } catch (_) {}
37
- }
38
- return null;
39
- }
40
-
41
- (async () => {
42
- // Resolve project cwd from hook input JSON
43
- await _stdinPromise;
44
- let _hookCwd = process.cwd();
45
- try { _hookCwd = JSON.parse(_stdinRaw).cwd || process.cwd(); } catch (_) {}
46
-
47
- const secrets = findSecrets();
48
- if (!secrets) {
49
- // silently skip — QGSD may not be installed yet or keytar absent
50
- process.exit(0);
51
- }
52
- try {
53
- await secrets.syncToClaudeJson(secrets.SERVICE);
54
- } catch (e) {
55
- // Non-fatal — write to stderr for debug logs, but never block session start
56
- process.stderr.write('[qgsd-session-start] sync error: ' + e.message + '\n');
57
- }
58
-
59
- // Populate CCR config from keytar (fail-silent — CCR may not be installed)
60
- try {
61
- const { execFileSync } = require('child_process');
62
- const nodeFsRef = require('fs');
63
- const ccrCandidates = [
64
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'ccr-secure-config.cjs'),
65
- path.join(__dirname, '..', 'bin', 'ccr-secure-config.cjs'),
66
- ];
67
- let ccrConfigPath = null;
68
- for (const p of ccrCandidates) {
69
- if (nodeFsRef.existsSync(p)) { ccrConfigPath = p; break; }
70
- }
71
- if (ccrConfigPath) {
72
- execFileSync(process.execPath, [ccrConfigPath], { stdio: 'pipe', timeout: 10000 });
73
- }
74
- } catch (e) {
75
- process.stderr.write('[qgsd-session-start] CCR config error: ' + e.message + '\n');
76
- }
77
-
78
- // Telemetry surfacing — inject top unsurfaced issue as additionalContext
79
- // Guard: only active when running inside the QGSD dev repo itself
80
- try {
81
- const pkgPath = path.join(_hookCwd, 'package.json');
82
- const isQgsdRepo = fs.existsSync(pkgPath) &&
83
- JSON.parse(fs.readFileSync(pkgPath, 'utf8')).name === 'qgsd';
84
- const fixesPath = path.join(_hookCwd, '.planning', 'telemetry', 'pending-fixes.json');
85
- if (isQgsdRepo && fs.existsSync(fixesPath)) {
86
- const fixes = JSON.parse(fs.readFileSync(fixesPath, 'utf8'));
87
- const issue = (fixes.issues || []).find(i => !i.surfaced && i.priority >= 50);
88
- if (issue) {
89
- issue.surfaced = true;
90
- issue.surfacedAt = new Date().toISOString();
91
- fs.writeFileSync(fixesPath, JSON.stringify(fixes, null, 2), 'utf8');
92
- const ctx = 'Telemetry alert [priority=' + issue.priority + ']: ' + issue.description + '\nSuggested fix: ' + issue.action;
93
- process.stdout.write(JSON.stringify({
94
- hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: ctx }
95
- }));
96
- }
97
- }
98
- } catch (_) {}
99
-
100
- // Memory staleness check — warn about outdated MEMORY.md entries
101
- try {
102
- const validateMemoryCandidates = [
103
- path.join(os.homedir(), '.claude', 'qgsd-bin', 'validate-memory.cjs'),
104
- path.join(__dirname, '..', 'bin', 'validate-memory.cjs'),
105
- ];
106
- let validateMemoryMod = null;
107
- for (const p of validateMemoryCandidates) {
108
- try { validateMemoryMod = require(p); break; } catch (_) {}
109
- }
110
- if (validateMemoryMod) {
111
- const { findings } = validateMemoryMod.validateMemory({ cwd: _hookCwd, quiet: true });
112
- if (findings.length > 0) {
113
- const summary = findings
114
- .map(f => '[memory-check] ' + f.message)
115
- .join('\n');
116
- process.stderr.write(summary + '\n');
117
- }
118
- }
119
- } catch (_) {}
120
-
121
- process.exit(0);
122
- })();