@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
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ // hooks/nf-session-start.js
3
+ // SessionStart hook — syncs nForma 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
+ const { loadConfig, shouldRunHook } = require('./config-loader');
15
+
16
+ // ─── Stdin accumulation (for hook input JSON containing cwd) ─────────────────
17
+ let _stdinRaw = '';
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', c => _stdinRaw += c);
20
+
21
+ let _stdinReady;
22
+ const _stdinPromise = new Promise(resolve => { _stdinReady = resolve; });
23
+ process.stdin.on('end', () => _stdinReady());
24
+
25
+ // Locate secrets.cjs — try installed global path first, then local dev path.
26
+ //
27
+ // IMPORTANT: install.js copies bin/*.cjs to ~/.claude/nf-bin/ (not ~/.claude/nf/bin/).
28
+ // See bin/install.js line ~1679: binDest = path.join(targetDir, 'nf-bin')
29
+ // where targetDir = os.homedir() + '/.claude'.
30
+ function findSecrets() {
31
+ const candidates = [
32
+ path.join(os.homedir(), '.claude', 'nf-bin', 'secrets.cjs'), // installed path
33
+ path.join(__dirname, '..', 'bin', 'secrets.cjs'), // local dev path
34
+ ];
35
+ for (const p of candidates) {
36
+ try {
37
+ return require(p);
38
+ } catch (_) {}
39
+ }
40
+ return null;
41
+ }
42
+
43
+ // ─── State reminder parser ──────────────────────────────────────────────────
44
+
45
+ /**
46
+ * Parse STATE.md content for an in-progress phase and return a terse reminder.
47
+ * Returns null if no reminder is needed (complete, not started, or missing fields).
48
+ * @param {string} stateContent - Raw STATE.md content.
49
+ * @returns {string|null}
50
+ */
51
+ function parseStateForReminder(stateContent) {
52
+ if (!stateContent || typeof stateContent !== 'string') return null;
53
+
54
+ const phaseMatch = stateContent.match(/Phase:\s*(.+)/);
55
+ const statusMatch = stateContent.match(/Status:\s*(.+)/);
56
+ const planMatch = stateContent.match(/Plan:\s*(.+)/);
57
+ const lastMatch = stateContent.match(/Last activity:\s*(.+)/);
58
+
59
+ if (!phaseMatch) return null;
60
+ if (!statusMatch) return null;
61
+
62
+ const status = statusMatch[1].trim();
63
+ if (status === 'Complete' || status === 'Not started') return null;
64
+
65
+ const phase = phaseMatch[1].trim();
66
+ const plan = planMatch ? planMatch[1].trim() : 'unknown plan';
67
+ const lastActivity = lastMatch ? lastMatch[1].trim() : 'unknown';
68
+
69
+ return 'SESSION STATE REMINDER: Phase ' + phase + ' -- ' + plan + ' -- ' + status + ' (last: ' + lastActivity + ')';
70
+ }
71
+
72
+ (async () => {
73
+ // Resolve project cwd from hook input JSON
74
+ await _stdinPromise;
75
+ let _hookCwd = process.cwd();
76
+ try { _hookCwd = JSON.parse(_stdinRaw).cwd || process.cwd(); } catch (_) {}
77
+
78
+ // Profile guard — exit early if this hook is not active for the current profile
79
+ const config = loadConfig(_hookCwd);
80
+ const profile = config.hook_profile || 'standard';
81
+ if (!shouldRunHook('nf-session-start', profile)) {
82
+ process.exit(0);
83
+ }
84
+
85
+ const secrets = findSecrets();
86
+ if (!secrets) {
87
+ // silently skip — nForma may not be installed yet or keytar absent
88
+ process.exit(0);
89
+ }
90
+ try {
91
+ await secrets.syncToClaudeJson(secrets.SERVICE);
92
+ } catch (e) {
93
+ // Non-fatal — write to stderr for debug logs, but never block session start
94
+ process.stderr.write('[nf-session-start] sync error: ' + e.message + '\n');
95
+ }
96
+
97
+ // Populate CCR config from keytar (fail-silent — CCR may not be installed)
98
+ try {
99
+ const { execFileSync } = require('child_process');
100
+ const nodeFsRef = require('fs');
101
+ const ccrCandidates = [
102
+ path.join(os.homedir(), '.claude', 'nf-bin', 'ccr-secure-config.cjs'),
103
+ path.join(__dirname, '..', 'bin', 'ccr-secure-config.cjs'),
104
+ ];
105
+ let ccrConfigPath = null;
106
+ for (const p of ccrCandidates) {
107
+ if (nodeFsRef.existsSync(p)) { ccrConfigPath = p; break; }
108
+ }
109
+ if (ccrConfigPath) {
110
+ execFileSync(process.execPath, [ccrConfigPath], { stdio: 'pipe', timeout: 10000 });
111
+ }
112
+ } catch (e) {
113
+ process.stderr.write('[nf-session-start] CCR config error: ' + e.message + '\n');
114
+ }
115
+
116
+ // Collect all additionalContext pieces — write once at the end
117
+ const _contextPieces = [];
118
+
119
+ // Telemetry surfacing — inject top unsurfaced issue as additionalContext
120
+ // Guard: only active when running inside the nForma dev repo itself
121
+ try {
122
+ const pkgPath = path.join(_hookCwd, 'package.json');
123
+ const isNfRepo = fs.existsSync(pkgPath) &&
124
+ JSON.parse(fs.readFileSync(pkgPath, 'utf8')).name === 'nforma';
125
+ const fixesPath = path.join(_hookCwd, '.planning', 'telemetry', 'pending-fixes.json');
126
+ if (isNfRepo && fs.existsSync(fixesPath)) {
127
+ const fixes = JSON.parse(fs.readFileSync(fixesPath, 'utf8'));
128
+ const issue = (fixes.issues || []).find(i => !i.surfaced && i.priority >= 50);
129
+ if (issue) {
130
+ issue.surfaced = true;
131
+ issue.surfacedAt = new Date().toISOString();
132
+ fs.writeFileSync(fixesPath, JSON.stringify(fixes, null, 2), 'utf8');
133
+ _contextPieces.push('Telemetry alert [priority=' + issue.priority + ']: ' + issue.description + '\nSuggested fix: ' + issue.action);
134
+ }
135
+ }
136
+ } catch (_) {}
137
+
138
+ // Session state reminder — inject brief context when work is in progress
139
+ try {
140
+ const statePath = path.join(_hookCwd, '.planning', 'STATE.md');
141
+ if (fs.existsSync(statePath)) {
142
+ const stateContent = fs.readFileSync(statePath, 'utf8');
143
+ const reminder = parseStateForReminder(stateContent);
144
+ if (reminder) {
145
+ _contextPieces.push(reminder);
146
+ }
147
+ }
148
+ } catch (_) {}
149
+
150
+ // Write combined additionalContext output (once)
151
+ if (_contextPieces.length > 0) {
152
+ process.stdout.write(JSON.stringify({
153
+ hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: _contextPieces.join('\n\n') }
154
+ }));
155
+ }
156
+
157
+ // Memory staleness check — warn about outdated MEMORY.md entries
158
+ try {
159
+ const validateMemoryCandidates = [
160
+ path.join(os.homedir(), '.claude', 'nf-bin', 'validate-memory.cjs'),
161
+ path.join(__dirname, '..', 'bin', 'validate-memory.cjs'),
162
+ ];
163
+ let validateMemoryMod = null;
164
+ for (const p of validateMemoryCandidates) {
165
+ try { validateMemoryMod = require(p); break; } catch (_) {}
166
+ }
167
+ if (validateMemoryMod) {
168
+ const { findings } = validateMemoryMod.validateMemory({ cwd: _hookCwd, quiet: true });
169
+ if (findings.length > 0) {
170
+ const summary = findings
171
+ .map(f => '[memory-check] ' + f.message)
172
+ .join('\n');
173
+ process.stderr.write(summary + '\n');
174
+ }
175
+ }
176
+ } catch (_) {}
177
+
178
+ process.exit(0);
179
+ })();
180
+
181
+ // Export for unit testing
182
+ if (typeof module !== 'undefined') {
183
+ module.exports = module.exports || {};
184
+ module.exports.parseStateForReminder = parseStateForReminder;
185
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-slot-correlator.js
3
- // SubagentStart hook — writes a correlation placeholder file for qgsd-quorum-slot-worker subagents.
2
+ // hooks/nf-slot-correlator.js
3
+ // SubagentStart hook — writes a correlation placeholder file for nf-quorum-slot-worker subagents.
4
4
  //
5
5
  // At SubagentStart time, the prompt/slot name is not available in the hook payload.
6
6
  // This hook writes a stub correlation file { agent_id, ts, slot: null } so the
@@ -8,7 +8,7 @@
8
8
  // The slot is resolved from last_assistant_message preamble by the token collector.
9
9
  //
10
10
  // Guards:
11
- // - Only processes agent_type === 'qgsd-quorum-slot-worker' (exits 0 otherwise)
11
+ // - Only processes agent_type === 'nf-quorum-slot-worker' (exits 0 otherwise)
12
12
  // - If agent_id is absent: exits 0 gracefully
13
13
  // - Fail-open: any unhandled error exits 0
14
14
 
@@ -16,6 +16,7 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const { loadConfig, shouldRunHook } = require('./config-loader');
19
20
 
20
21
  function main() {
21
22
  let raw = '';
@@ -25,8 +26,15 @@ function main() {
25
26
  try {
26
27
  const input = JSON.parse(raw);
27
28
 
28
- // Guard: only process qgsd-quorum-slot-worker subagents
29
- if (input.agent_type !== 'qgsd-quorum-slot-worker') {
29
+ // Profile guard exit early if this hook is not active for the current profile
30
+ const config = loadConfig();
31
+ const profile = config.hook_profile || 'standard';
32
+ if (!shouldRunHook('nf-slot-correlator', profile)) {
33
+ process.exit(0);
34
+ }
35
+
36
+ // Guard: only process nf-quorum-slot-worker subagents
37
+ if (input.agent_type !== 'nf-quorum-slot-worker') {
30
38
  process.exit(0);
31
39
  }
32
40
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
- // hooks/qgsd-spec-regen.js
3
- // PostToolUse hook: when Claude writes to qgsd-workflow.machine.ts,
2
+ // hooks/nf-spec-regen.js
3
+ // PostToolUse hook: when Claude writes to nf-workflow.machine.ts,
4
4
  // automatically trigger generate-formal-specs.cjs to regenerate TLA+/Alloy specs.
5
5
  //
6
6
  // LOOP-02 (v0.21-03): Self-Calibrating Feedback Loops
@@ -16,6 +16,7 @@
16
16
  const { spawnSync } = require('child_process');
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const { loadConfig, shouldRunHook } = require('./config-loader');
19
20
 
20
21
  let raw = '';
21
22
  process.stdin.setEncoding('utf8');
@@ -23,11 +24,19 @@ process.stdin.on('data', (chunk) => { raw += chunk; });
23
24
  process.stdin.on('end', () => {
24
25
  try {
25
26
  const input = JSON.parse(raw);
27
+
28
+ // Profile guard — exit early if this hook is not active for the current profile
29
+ const config = loadConfig(input.cwd || process.cwd());
30
+ const profile = config.hook_profile || 'standard';
31
+ if (!shouldRunHook('nf-spec-regen', profile)) {
32
+ process.exit(0);
33
+ }
34
+
26
35
  const toolName = input.tool_name || '';
27
36
  const filePath = (input.tool_input && input.tool_input.file_path) || '';
28
37
 
29
- // Only act on Write calls to qgsd-workflow.machine.ts
30
- if (toolName !== 'Write' || !filePath.includes('qgsd-workflow.machine.ts')) {
38
+ // Only act on Write calls to nf-workflow.machine.ts
39
+ if (toolName !== 'Write' || !filePath.includes('nf-workflow.machine.ts')) {
31
40
  process.exit(0); // No-op — not a machine file write
32
41
  }
33
42
 
@@ -50,16 +59,16 @@ process.stdin.on('end', () => {
50
59
  (result.error ? String(result.error) : '');
51
60
  }
52
61
 
53
- // Also regenerate QGSDQuorum_xstate.tla (xstate-to-tla.cjs)
62
+ // Also regenerate NFQuorum_xstate.tla (xstate-to-tla.cjs)
54
63
  const xstateScript = path.join(cwd, 'bin', 'xstate-to-tla.cjs');
55
- const machineFile = path.join(cwd, 'src', 'machines', 'qgsd-workflow.machine.ts');
56
- const guardsConfig = path.join(cwd, '.planning', 'formal', 'tla', 'guards', 'qgsd-workflow.json');
64
+ const machineFile = path.join(cwd, 'src', 'machines', 'nf-workflow.machine.ts');
65
+ const guardsConfig = path.join(cwd, '.planning', 'formal', 'tla', 'guards', 'nf-workflow.json');
57
66
 
58
67
  if (fs.existsSync(xstateScript) && fs.existsSync(guardsConfig)) {
59
68
  const xstateResult = spawnSync(process.execPath, [
60
69
  xstateScript, machineFile,
61
70
  '--config=' + guardsConfig,
62
- '--module=QGSDQuorum'
71
+ '--module=NFQuorum'
63
72
  ], {
64
73
  encoding: 'utf8',
65
74
  cwd: cwd,
@@ -5,6 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const os = require('os');
8
+ const { loadConfig, shouldRunHook } = require('./config-loader');
8
9
 
9
10
  // Read JSON from stdin
10
11
  let input = '';
@@ -13,6 +14,14 @@ process.stdin.on('data', chunk => input += chunk);
13
14
  process.stdin.on('end', () => {
14
15
  try {
15
16
  const data = JSON.parse(input);
17
+
18
+ // Profile guard — exit early if this hook is not active for the current profile
19
+ const config = loadConfig(data.workspace?.current_dir || process.cwd());
20
+ const profile = config.hook_profile || 'standard';
21
+ if (!shouldRunHook('nf-statusline', profile)) {
22
+ process.exit(0);
23
+ }
24
+
16
25
  const model = data.model?.display_name || 'Claude';
17
26
  const dir = data.workspace?.current_dir || process.cwd();
18
27
  const session = data.session_id || '';
@@ -66,14 +75,14 @@ process.stdin.on('end', () => {
66
75
  }
67
76
  }
68
77
 
69
- // QGSD update available?
78
+ // nForma update available?
70
79
  let gsdUpdate = '';
71
- const cacheFile = path.join(homeDir, '.claude', 'cache', 'qgsd-update-check.json');
80
+ const cacheFile = path.join(homeDir, '.claude', 'cache', 'nf-update-check.json');
72
81
  if (fs.existsSync(cacheFile)) {
73
82
  try {
74
83
  const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
75
84
  if (cache.update_available) {
76
- gsdUpdate = '\x1b[33m⬆ /qgsd:update\x1b[0m │ ';
85
+ gsdUpdate = '\x1b[33m⬆ /nf:update\x1b[0m │ ';
77
86
  }
78
87
  } catch (e) {}
79
88
  }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // hooks/qgsd-stop.js
2
+ // hooks/nf-stop.js
3
3
  // Stop hook — quorum verification gate for GSD planning commands.
4
4
  //
5
5
  // Reads JSON from stdin (Claude Code Stop event payload), applies guards in
@@ -7,9 +7,9 @@
7
7
  // evidence. Blocks with decision:block if a planning command was issued but
8
8
  // quorum tool calls are missing. Fails open on all errors.
9
9
  //
10
- // Config: ~/.claude/qgsd.json (two-layer merge via shared config-loader)
10
+ // Config: ~/.claude/nf.json (two-layer merge via shared config-loader)
11
11
  // Unavailability: reads ~/.claude.json mcpServers to detect which models are installed
12
- // (QGSD_CLAUDE_JSON env var overrides the path — for testing only)
12
+ // (NF_CLAUDE_JSON env var overrides the path — for testing only)
13
13
 
14
14
  'use strict';
15
15
 
@@ -17,9 +17,15 @@ const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
19
 
20
- const { loadConfig, DEFAULT_CONFIG, slotToToolCall } = require('./config-loader');
20
+ const { loadConfig, DEFAULT_CONFIG, slotToToolCall, shouldRunHook } = require('./config-loader');
21
21
  const { schema_version } = require('./conformance-schema.cjs');
22
22
 
23
+ // Cache module — fail-open: if require fails, all cache logic is skipped
24
+ let cacheModule = null;
25
+ try {
26
+ cacheModule = require(path.join(__dirname, '..', 'bin', 'quorum-cache.cjs'));
27
+ } catch (_) { /* fail-open: cache unavailable */ }
28
+
23
29
  // Appends a structured conformance event to .planning/conformance-events.jsonl.
24
30
  // Uses appendFileSync (atomic for writes < POSIX PIPE_BUF = 4096 bytes).
25
31
  // Always wrapped in try/catch — hooks are fail-open; never crashes on logging failure.
@@ -30,14 +36,14 @@ function appendConformanceEvent(event) {
30
36
  const logPath = pp.resolve(process.cwd(), 'conformance-events');
31
37
  fs.appendFileSync(logPath, JSON.stringify(event) + '\n', 'utf8');
32
38
  } catch (err) {
33
- process.stderr.write('[qgsd] conformance log write failed: ' + err.message + '\n');
39
+ process.stderr.write('[nf] conformance log write failed: ' + err.message + '\n');
34
40
  }
35
41
  }
36
42
 
37
- // Builds the regex that matches /gsd:<quorum-command> or /qgsd:<quorum-command> in any text.
43
+ // Builds the regex that matches /nf:<cmd>, /gsd:<cmd>, or /qgsd:<cmd> in any text.
38
44
  function buildCommandPattern(quorumCommands) {
39
45
  const escaped = quorumCommands.map(c => c.replace(/-/g, '\\-'));
40
- return new RegExp('\\/q?gsd:(' + escaped.join('|') + ')');
46
+ return new RegExp('\\/(nf|q?gsd):(' + escaped.join('|') + ')');
41
47
  }
42
48
 
43
49
  // Returns true if a parsed JSONL user entry is a human text message.
@@ -119,9 +125,9 @@ function hasQuorumCommand(currentTurnLines, cmdPattern) {
119
125
  return false;
120
126
  }
121
127
 
122
- // Extracts the matched /gsd:<command> or /qgsd:<command> text from the first matching user line.
128
+ // Extracts the matched /nf:<cmd>, /gsd:<cmd>, or /qgsd:<cmd> text from the first matching user line.
123
129
  // Uses XML-tag-first strategy: prefers the <command-name> tag for accurate command identification.
124
- // Falls back to first 300 chars of message text, then to '/qgsd:plan-phase' as ultimate fallback.
130
+ // Falls back to first 300 chars of message text, then to '/nf:plan-phase' as ultimate fallback.
125
131
  function extractCommand(currentTurnLines, cmdPattern) {
126
132
  for (const line of currentTurnLines) {
127
133
  try {
@@ -148,12 +154,12 @@ function extractCommand(currentTurnLines, cmdPattern) {
148
154
  // Skip malformed lines
149
155
  }
150
156
  }
151
- return '/qgsd:plan-phase';
157
+ return '/nf:plan-phase';
152
158
  }
153
159
 
154
- // Returns true if any assistant turn used Task(subagent_type=qgsd-quorum-slot-worker).
160
+ // Returns true if any assistant turn used Task(subagent_type=nf-quorum-slot-worker).
155
161
  // Slot-workers are the inline dispatch mechanism — one per active slot per round.
156
- // Replaced qgsd-quorum-orchestrator (deprecated quick-103) as full quorum evidence.
162
+ // Replaced nf-quorum-orchestrator (deprecated quick-103) as full quorum evidence.
157
163
  function wasSlotWorkerUsed(currentTurnLines) {
158
164
  for (const line of currentTurnLines) {
159
165
  try {
@@ -165,7 +171,7 @@ function wasSlotWorkerUsed(currentTurnLines) {
165
171
  if (block.type !== 'tool_use' || block.name !== 'Task') continue;
166
172
  const input = block.input || {};
167
173
  const subagentType = input.subagent_type || input.subagentType || '';
168
- if (subagentType === 'qgsd-quorum-slot-worker') return true;
174
+ if (subagentType === 'nf-quorum-slot-worker') return true;
169
175
  }
170
176
  } catch { /* skip */ }
171
177
  }
@@ -281,7 +287,7 @@ function buildAgentPool(config) {
281
287
  // Returns an array of derived tool prefixes (e.g. ['mcp__codex-cli-1__', 'mcp__gemini-cli-1__']).
282
288
  // Returns null if the file is missing or malformed — callers treat null as "unknown" (conservative).
283
289
  //
284
- // TESTING ONLY: set QGSD_CLAUDE_JSON env var to override the file path.
290
+ // TESTING ONLY: set NF_CLAUDE_JSON env var to override the file path.
285
291
  // In production, always reads ~/.claude.json.
286
292
  //
287
293
  // KNOWN LIMITATION: Only reads ~/.claude.json (user-scoped MCPs). Project-scoped MCPs
@@ -289,14 +295,14 @@ function buildAgentPool(config) {
289
295
  // project level, it will be classified as unavailable and skipped (fail-open).
290
296
  // In practice, quorum models (Codex, Gemini, OpenCode) are global tools.
291
297
  function getAvailableMcpPrefixes() {
292
- const claudeJsonPath = process.env.QGSD_CLAUDE_JSON || path.join(os.homedir(), '.claude.json');
298
+ const claudeJsonPath = process.env.NF_CLAUDE_JSON || path.join(os.homedir(), '.claude.json');
293
299
  if (!fs.existsSync(claudeJsonPath)) return null;
294
300
  try {
295
301
  const d = JSON.parse(fs.readFileSync(claudeJsonPath, 'utf8'));
296
302
  const servers = d.mcpServers || {};
297
303
  return Object.keys(servers).map(name => 'mcp__' + name + '__');
298
304
  } catch (e) {
299
- process.stderr.write('[qgsd] WARNING: Could not parse ~/.claude.json: ' + e.message + '\n');
305
+ process.stderr.write('[nf] WARNING: Could not parse ~/.claude.json: ' + e.message + '\n');
300
306
  return null;
301
307
  }
302
308
  }
@@ -313,6 +319,35 @@ const ARTIFACT_PATTERNS = [
313
319
  /PROJECT\.md/, // PROJECT.md (new-project early commit)
314
320
  ];
315
321
 
322
+ // Counts the number of distinct quorum dispatch rounds in the current turn.
323
+ // A dispatch round is one assistant message that contains at least one Task tool_use
324
+ // block targeting nf-quorum-slot-worker. Multiple parallel dispatches within one
325
+ // assistant message count as a single round (they are dispatched simultaneously).
326
+ // Returns Math.max(1, rounds) — at least round 1 even if no explicit dispatch detected.
327
+ function countDeliberationRounds(currentTurnLines) {
328
+ let rounds = 0;
329
+ for (const line of currentTurnLines) {
330
+ try {
331
+ const entry = JSON.parse(line);
332
+ if (entry.type !== 'assistant') continue;
333
+ const content = entry.message && entry.message.content;
334
+ if (!Array.isArray(content)) continue;
335
+ let hasSlotWorker = false;
336
+ for (const block of content) {
337
+ if (block.type === 'tool_use' && block.name === 'Task') {
338
+ const inputStr = JSON.stringify(block.input || {});
339
+ if (inputStr.includes('nf-quorum-slot-worker')) {
340
+ hasSlotWorker = true;
341
+ break; // One per assistant message — no need to check more blocks
342
+ }
343
+ }
344
+ }
345
+ if (hasSlotWorker) rounds++;
346
+ } catch { /* skip malformed lines */ }
347
+ }
348
+ return Math.max(1, rounds);
349
+ }
350
+
316
351
  // Returns true if the current turn contains a Bash tool_use block that BOTH:
317
352
  // (a) invokes gsd-tools.cjs commit, AND
318
353
  // (b) references a planning artifact file path (not codebase/*.md).
@@ -401,6 +436,54 @@ function deriveMissingToolName(modelKey, modelDef) {
401
436
  return prefix + modelKey;
402
437
  }
403
438
 
439
+ // Returns true if any user entry in currentTurnLines contains the NF_CACHE_HIT marker.
440
+ // This marker is injected by nf-prompt.js when a valid cache hit is found.
441
+ function hasCacheHitMarker(currentTurnLines) {
442
+ for (const line of currentTurnLines) {
443
+ try {
444
+ const entry = JSON.parse(line);
445
+ if (entry.type !== 'user') continue;
446
+ const content = entry.message?.content;
447
+ let text = '';
448
+ if (typeof content === 'string') {
449
+ text = content;
450
+ } else if (Array.isArray(content)) {
451
+ text = content
452
+ .filter(c => c?.type === 'text')
453
+ .map(c => c.text || '')
454
+ .join('');
455
+ }
456
+ if (text.includes('<!-- NF_CACHE_HIT -->')) return true;
457
+ } catch { /* skip malformed lines */ }
458
+ }
459
+ return false;
460
+ }
461
+
462
+ // Extracts the cache key from NF_CACHE_KEY marker in currentTurnLines.
463
+ // Returns the hex string or null if not found.
464
+ function extractCacheKey(currentTurnLines) {
465
+ const keyPattern = /<!-- NF_CACHE_KEY:([a-f0-9]+) -->/;
466
+ for (const line of currentTurnLines) {
467
+ try {
468
+ const entry = JSON.parse(line);
469
+ if (entry.type !== 'user') continue;
470
+ const content = entry.message?.content;
471
+ let text = '';
472
+ if (typeof content === 'string') {
473
+ text = content;
474
+ } else if (Array.isArray(content)) {
475
+ text = content
476
+ .filter(c => c?.type === 'text')
477
+ .map(c => c.text || '')
478
+ .join('');
479
+ }
480
+ const m = text.match(keyPattern);
481
+ if (m) return m[1];
482
+ } catch { /* skip malformed lines */ }
483
+ }
484
+ return null;
485
+ }
486
+
404
487
  function main() {
405
488
  let raw = '';
406
489
  process.stdin.setEncoding('utf8');
@@ -426,6 +509,12 @@ function main() {
426
509
 
427
510
  const config = loadConfig();
428
511
 
512
+ // Profile guard — exit early if this hook is not active for the current profile
513
+ const profile = config.hook_profile || 'standard';
514
+ if (!shouldRunHook('nf-stop', profile)) {
515
+ process.exit(0);
516
+ }
517
+
429
518
  // Read and split transcript JSONL; skip empty lines
430
519
  const lines = fs.readFileSync(input.transcript_path, 'utf8')
431
520
  .split('\n')
@@ -435,7 +524,10 @@ function main() {
435
524
  const currentTurnLines = getCurrentTurnLines(lines);
436
525
 
437
526
  // Build command pattern once; reuse for detection and extraction
438
- const cmdPattern = buildCommandPattern(config.quorum_commands);
527
+ // Strict mode: match ANY /nf: or /gsd: or /qgsd: command, not just quorum_commands list.
528
+ const cmdPattern = profile === 'strict'
529
+ ? /\/(nf|q?gsd):[\w][\w-]*/
530
+ : buildCommandPattern(config.quorum_commands);
439
531
 
440
532
  // GUARD 4: Only enforce quorum if a planning command is in current turn (STOP-06)
441
533
  if (!hasQuorumCommand(currentTurnLines, cmdPattern)) {
@@ -469,6 +561,24 @@ function main() {
469
561
  process.exit(0); // Solo mode: Claude's vote is the quorum — no block
470
562
  }
471
563
 
564
+ // GUARD 7: Cache hit bypass — nf-prompt.js validated the cache entry and injected
565
+ // the NF_CACHE_HIT marker. Trust it: the entry was completed, within TTL, and
566
+ // matches git HEAD + quorum_active composition (EventualConsensus preserved).
567
+ if (hasCacheHitMarker(currentTurnLines)) {
568
+ appendConformanceEvent({
569
+ ts: new Date().toISOString(),
570
+ phase: 'DECIDING',
571
+ action: 'quorum_complete',
572
+ cache_hit: true,
573
+ pass_at_k: 0,
574
+ slots_available: 0,
575
+ vote_result: 0,
576
+ outcome: 'APPROVE',
577
+ schema_version,
578
+ });
579
+ process.exit(0); // Cache hit: quorum approved via cached result
580
+ }
581
+
472
582
  // Build agent pool from config
473
583
  const agentPool = buildAgentPool(config);
474
584
 
@@ -481,7 +591,7 @@ function main() {
481
591
  }
482
592
 
483
593
  // Ceiling: require maxSize successful (non-error) responses from the full pool.
484
- // Named maxSize for consistency with qgsd-prompt.js and the config schema.
594
+ // Named maxSize for consistency with nf-prompt.js and the config schema.
485
595
  // --n N override: N total participants means N-1 external models required.
486
596
  const maxSize = quorumSizeOverride !== null && quorumSizeOverride > 1
487
597
  ? quorumSizeOverride - 1 // --n N means N-1 external models required
@@ -532,10 +642,34 @@ function main() {
532
642
  process.exit(0);
533
643
  }
534
644
 
645
+ // Cache backfill: update pending cache entry with vote result after successful quorum
646
+ if (cacheModule) {
647
+ try {
648
+ const cKey = extractCacheKey(currentTurnLines);
649
+ if (cKey) {
650
+ const cacheDir = path.join(process.cwd(), '.planning', '.quorum-cache');
651
+ const pendingEntry = cacheModule.readCache(cKey, cacheDir);
652
+ // readCache returns null for pending entries (no completed field) — read raw instead
653
+ const cacheFilePath = path.join(cacheDir, `${cKey}.json`);
654
+ if (fs.existsSync(cacheFilePath)) {
655
+ const rawEntry = JSON.parse(fs.readFileSync(cacheFilePath, 'utf8'));
656
+ rawEntry.vote_result = successCount;
657
+ rawEntry.outcome = 'APPROVE';
658
+ rawEntry.completed = new Date().toISOString();
659
+ cacheModule.writeCache(cKey, rawEntry, cacheDir);
660
+ }
661
+ }
662
+ } catch (backfillErr) {
663
+ // Fail-open: backfill failure never blocks quorum approval
664
+ process.stderr.write('[nf] cache backfill failed (fail-open): ' + (backfillErr.message || backfillErr) + '\n');
665
+ }
666
+ }
667
+
535
668
  appendConformanceEvent({
536
669
  ts: new Date().toISOString(),
537
670
  phase: 'DECIDING',
538
671
  action: 'quorum_complete',
672
+ pass_at_k: countDeliberationRounds(currentTurnLines),
539
673
  slots_available: agentPool.length,
540
674
  vote_result: successCount,
541
675
  outcome: 'APPROVE',
@@ -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