@nforma.ai/nforma 0.28.0 → 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.
- package/bin/quorum-preflight.cjs +89 -0
- package/commands/nf/quorum.md +3 -53
- package/package.json +4 -2
- package/hooks/dist/nf-circuit-breaker.test.js +0 -1002
- package/hooks/dist/nf-precompact.test.js +0 -227
- package/hooks/dist/nf-prompt.test.js +0 -698
- package/hooks/dist/nf-session-start.test.js +0 -354
- package/hooks/dist/nf-slot-correlator.test.js +0 -85
- package/hooks/dist/nf-spec-regen.test.js +0 -73
- package/hooks/dist/nf-statusline.test.js +0 -157
- package/hooks/dist/nf-stop.test.js +0 -1388
- package/hooks/dist/nf-token-collector.test.js +0 -262
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* quorum-preflight.cjs — extract quorum config and team identity from nf.json + providers.json
|
|
6
|
+
*
|
|
7
|
+
* Replaces three inline `node -e` snippets in quorum.md that caused shell-escaping
|
|
8
|
+
* failures (LLMs escaping `!` as `\!` inside node -e strings).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node quorum-preflight.cjs --quorum-active # → JSON array of active slot names
|
|
12
|
+
* node quorum-preflight.cjs --max-quorum-size # → integer (default 3)
|
|
13
|
+
* node quorum-preflight.cjs --team # → JSON { slotName: { model } }
|
|
14
|
+
* node quorum-preflight.cjs --all # → JSON { quorum_active, max_quorum_size, team }
|
|
15
|
+
*
|
|
16
|
+
* All modes read from ~/.claude/nf.json (global) merged with $CWD/.claude/nf.json (project).
|
|
17
|
+
* --team and --all also read providers.json (same search logic as call-quorum-slot.cjs).
|
|
18
|
+
*
|
|
19
|
+
* Exit code: always 0. Output: JSON to stdout.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
|
|
26
|
+
// ─── Read merged nf.json config ─────────────────────────────────────────────
|
|
27
|
+
function readConfig() {
|
|
28
|
+
const globalCfg = path.join(os.homedir(), '.claude', 'nf.json');
|
|
29
|
+
const projCfg = path.join(process.cwd(), '.claude', 'nf.json');
|
|
30
|
+
let cfg = {};
|
|
31
|
+
for (const f of [globalCfg, projCfg]) {
|
|
32
|
+
try { Object.assign(cfg, JSON.parse(fs.readFileSync(f, 'utf8'))); } catch (_) {}
|
|
33
|
+
}
|
|
34
|
+
return cfg;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Find providers.json (mirrors call-quorum-slot.cjs / probe-quorum-slots.cjs) ──
|
|
38
|
+
function findProviders() {
|
|
39
|
+
const searchPaths = [
|
|
40
|
+
path.join(__dirname, 'providers.json'),
|
|
41
|
+
path.join(os.homedir(), '.claude', 'nf-bin', 'providers.json'),
|
|
42
|
+
];
|
|
43
|
+
try {
|
|
44
|
+
const claudeJson = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
45
|
+
const u1args = claudeJson?.mcpServers?.['unified-1']?.args ?? [];
|
|
46
|
+
const serverScript = u1args.find(a => typeof a === 'string' && a.endsWith('unified-mcp-server.mjs'));
|
|
47
|
+
if (serverScript) searchPaths.unshift(path.join(path.dirname(serverScript), 'providers.json'));
|
|
48
|
+
} catch (_) {}
|
|
49
|
+
for (const p of searchPaths) {
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, 'utf8')).providers;
|
|
52
|
+
} catch (_) {}
|
|
53
|
+
}
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Build team JSON from providers + config ────────────────────────────────
|
|
58
|
+
function buildTeam(providers, active) {
|
|
59
|
+
const team = {};
|
|
60
|
+
for (const p of providers) {
|
|
61
|
+
if (active.length > 0 && !active.includes(p.name)) continue;
|
|
62
|
+
team[p.name] = { model: p.model };
|
|
63
|
+
}
|
|
64
|
+
return team;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Main ───────────────────────────────────────────────────────────────────
|
|
68
|
+
const mode = process.argv[2] || '--all';
|
|
69
|
+
const cfg = readConfig();
|
|
70
|
+
|
|
71
|
+
if (mode === '--quorum-active') {
|
|
72
|
+
console.log(JSON.stringify(cfg.quorum_active || []));
|
|
73
|
+
} else if (mode === '--max-quorum-size') {
|
|
74
|
+
console.log(cfg.max_quorum_size ?? 3);
|
|
75
|
+
} else if (mode === '--team') {
|
|
76
|
+
const providers = findProviders();
|
|
77
|
+
const active = cfg.quorum_active || [];
|
|
78
|
+
console.log(JSON.stringify(buildTeam(providers, active)));
|
|
79
|
+
} else if (mode === '--all') {
|
|
80
|
+
const providers = findProviders();
|
|
81
|
+
const active = cfg.quorum_active || [];
|
|
82
|
+
const team = buildTeam(providers, active);
|
|
83
|
+
const maxSize = cfg.max_quorum_size ?? 3;
|
|
84
|
+
console.log(JSON.stringify({ quorum_active: active, max_quorum_size: maxSize, team }));
|
|
85
|
+
} else {
|
|
86
|
+
console.error(`Unknown mode: ${mode}`);
|
|
87
|
+
console.error('Usage: node quorum-preflight.cjs [--quorum-active|--max-quorum-size|--team|--all]');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
package/commands/nf/quorum.md
CHANGED
|
@@ -75,16 +75,7 @@ Any server with `available: false` must be marked UNAVAIL immediately — skip i
|
|
|
75
75
|
|
|
76
76
|
3. **`$QUORUM_ACTIVE`**: read from `~/.claude/nf.json` (project config takes precedence over global):
|
|
77
77
|
```bash
|
|
78
|
-
node -
|
|
79
|
-
const fs = require('fs'), os = require('os'), path = require('path');
|
|
80
|
-
const globalCfg = path.join(os.homedir(), '.claude', 'nf.json');
|
|
81
|
-
const projCfg = path.join(process.cwd(), '.claude', 'nf.json');
|
|
82
|
-
let cfg = {};
|
|
83
|
-
for (const f of [globalCfg, projCfg]) {
|
|
84
|
-
try { Object.assign(cfg, JSON.parse(fs.readFileSync(f, 'utf8'))); } catch(_){}
|
|
85
|
-
}
|
|
86
|
-
console.log(JSON.stringify(cfg.quorum_active || []));
|
|
87
|
-
"
|
|
78
|
+
node "$HOME/.claude/nf-bin/quorum-preflight.cjs" --quorum-active
|
|
88
79
|
```
|
|
89
80
|
If `$QUORUM_ACTIVE` is empty (`[]`), all entries in `$CLAUDE_MCP_SERVERS` participate.
|
|
90
81
|
If non-empty, intersect: only servers whose `serverName` appears in `$QUORUM_ACTIVE` are called.
|
|
@@ -98,16 +89,7 @@ A server in `$QUORUM_ACTIVE` but absent from `$CLAUDE_MCP_SERVERS` = skip silent
|
|
|
98
89
|
|
|
99
90
|
**max_quorum_size check:** Read `max_quorum_size` from `~/.claude/nf.json` (project config takes precedence; default: 3 if absent):
|
|
100
91
|
```bash
|
|
101
|
-
node -
|
|
102
|
-
const fs = require('fs'), os = require('os'), path = require('path');
|
|
103
|
-
const globalCfg = path.join(os.homedir(), '.claude', 'nf.json');
|
|
104
|
-
const projCfg = path.join(process.cwd(), '.claude', 'nf.json');
|
|
105
|
-
let cfg = {};
|
|
106
|
-
for (const f of [globalCfg, projCfg]) {
|
|
107
|
-
try { Object.assign(cfg, JSON.parse(fs.readFileSync(f, 'utf8'))); } catch(_){}
|
|
108
|
-
}
|
|
109
|
-
console.log(cfg.max_quorum_size ?? 3);
|
|
110
|
-
"
|
|
92
|
+
node "$HOME/.claude/nf-bin/quorum-preflight.cjs" --max-quorum-size
|
|
111
93
|
```
|
|
112
94
|
Count available slots (those not marked UNAVAIL and passing $QUORUM_ACTIVE filter). Include Claude itself as +1.
|
|
113
95
|
If `availableCount < max_quorum_size`:
|
|
@@ -132,39 +114,7 @@ Provider pre-flight: <providerName>=✓/✗ ... (<N> claude-mcp servers found)
|
|
|
132
114
|
Before any quorum round, capture the active team fingerprint. Build TEAM_JSON directly from `providers.json` — no MCP calls needed for identity.
|
|
133
115
|
|
|
134
116
|
```bash
|
|
135
|
-
node -
|
|
136
|
-
const fs = require('fs'), path = require('path'), os = require('os');
|
|
137
|
-
|
|
138
|
-
const searchPaths = [
|
|
139
|
-
path.join(os.homedir(), '.claude', 'nf-bin', 'providers.json'),
|
|
140
|
-
];
|
|
141
|
-
try {
|
|
142
|
-
const cj = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
143
|
-
const u1args = cj?.mcpServers?.['unified-1']?.args ?? [];
|
|
144
|
-
const srv = u1args.find(a => typeof a === 'string' && a.endsWith('unified-mcp-server.mjs'));
|
|
145
|
-
if (srv) searchPaths.unshift(path.join(path.dirname(srv), 'providers.json'));
|
|
146
|
-
} catch(_) {}
|
|
147
|
-
|
|
148
|
-
let providers = [];
|
|
149
|
-
for (const p of searchPaths) {
|
|
150
|
-
try { providers = JSON.parse(fs.readFileSync(p, 'utf8')).providers; break; } catch(_) {}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const globalCfg = path.join(os.homedir(), '.claude', 'nf.json');
|
|
154
|
-
const projCfg = path.join(process.cwd(), '.claude', 'nf.json');
|
|
155
|
-
let cfg = {};
|
|
156
|
-
for (const f of [globalCfg, projCfg]) {
|
|
157
|
-
try { Object.assign(cfg, JSON.parse(fs.readFileSync(f, 'utf8'))); } catch(_) {}
|
|
158
|
-
}
|
|
159
|
-
const active = cfg.quorum_active || [];
|
|
160
|
-
|
|
161
|
-
const team = {};
|
|
162
|
-
for (const p of providers) {
|
|
163
|
-
if (active.length > 0 && !active.includes(p.name)) continue;
|
|
164
|
-
team[p.name] = { model: p.model };
|
|
165
|
-
}
|
|
166
|
-
console.log(JSON.stringify(team));
|
|
167
|
-
"
|
|
117
|
+
node "$HOME/.claude/nf-bin/quorum-preflight.cjs" --team
|
|
168
118
|
```
|
|
169
119
|
|
|
170
120
|
Store result as `TEAM_JSON`. Also build three lookup maps from `providers.json` for use during dispatch:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nforma.ai/nforma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"get-shit-done",
|
|
14
14
|
"agents",
|
|
15
15
|
"hooks/dist",
|
|
16
|
+
"!hooks/dist/*.test.*",
|
|
16
17
|
"scripts",
|
|
17
18
|
"!scripts/generate-logo-svg.js",
|
|
18
19
|
"!scripts/generate-terminal-svg.js",
|
|
@@ -88,7 +89,8 @@
|
|
|
88
89
|
"secrets:history": "bash scripts/secret-audit.sh",
|
|
89
90
|
"test": "npm run test:ci && npm run test:formal",
|
|
90
91
|
"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/
|
|
92
|
+
"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/nForma.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",
|
|
93
|
+
"test:install": "node --test test/install-virgin.test.cjs",
|
|
92
94
|
"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
95
|
"prepare": "husky"
|
|
94
96
|
}
|