@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.
- package/README.md +2 -2
- package/agents/{qgsd-codebase-mapper.md → nf-codebase-mapper.md} +1 -1
- package/agents/{qgsd-debugger.md → nf-debugger.md} +3 -3
- package/agents/{qgsd-executor.md → nf-executor.md} +14 -14
- package/agents/{qgsd-integration-checker.md → nf-integration-checker.md} +1 -1
- package/agents/{qgsd-phase-researcher.md → nf-phase-researcher.md} +6 -6
- package/agents/{qgsd-plan-checker.md → nf-plan-checker.md} +9 -9
- package/agents/{qgsd-planner.md → nf-planner.md} +9 -9
- package/agents/{qgsd-project-researcher.md → nf-project-researcher.md} +2 -2
- package/agents/{qgsd-quorum-orchestrator.md → nf-quorum-orchestrator.md} +33 -33
- package/agents/{qgsd-quorum-slot-worker.md → nf-quorum-slot-worker.md} +3 -3
- package/agents/{qgsd-quorum-synthesizer.md → nf-quorum-synthesizer.md} +3 -3
- package/agents/{qgsd-quorum-test-worker.md → nf-quorum-test-worker.md} +1 -1
- package/agents/{qgsd-quorum-worker.md → nf-quorum-worker.md} +6 -6
- package/agents/{qgsd-research-synthesizer.md → nf-research-synthesizer.md} +5 -5
- package/agents/{qgsd-roadmapper.md → nf-roadmapper.md} +3 -3
- package/agents/{qgsd-verifier.md → nf-verifier.md} +8 -8
- package/bin/accept-debug-invariant.cjs +2 -2
- package/bin/account-manager.cjs +10 -10
- package/bin/aggregate-requirements.cjs +1 -1
- package/bin/analyze-assumptions.cjs +3 -3
- package/bin/analyze-state-space.cjs +14 -14
- package/bin/assumption-register.cjs +146 -0
- package/bin/attribute-trace-divergence.cjs +1 -1
- package/bin/auth-drivers/gh-cli.cjs +1 -1
- package/bin/auth-drivers/pool.cjs +1 -1
- package/bin/autoClosePtoF.cjs +3 -3
- package/bin/budget-tracker.cjs +77 -0
- package/bin/build-layer-manifest.cjs +153 -0
- package/bin/call-quorum-slot.cjs +3 -3
- package/bin/ccr-secure-config.cjs +5 -5
- package/bin/check-bundled-sdks.cjs +1 -1
- package/bin/check-mcp-health.cjs +1 -1
- package/bin/check-provider-health.cjs +6 -6
- package/bin/check-spec-sync.cjs +26 -26
- package/bin/check-trace-schema-drift.cjs +5 -5
- package/bin/conformance-schema.cjs +2 -2
- package/bin/cross-layer-dashboard.cjs +297 -0
- package/bin/design-impact.cjs +377 -0
- package/bin/detect-coverage-gaps.cjs +7 -7
- package/bin/failure-mode-catalog.cjs +227 -0
- package/bin/failure-taxonomy.cjs +177 -0
- package/bin/formal-scope-scan.cjs +179 -0
- package/bin/gate-a-grounding.cjs +334 -0
- package/bin/gate-b-abstraction.cjs +243 -0
- package/bin/gate-c-validation.cjs +166 -0
- package/bin/generate-formal-specs.cjs +17 -17
- package/bin/generate-petri-net.cjs +3 -3
- package/bin/generate-tla-cfg.cjs +5 -5
- package/bin/git-heatmap.cjs +571 -0
- package/bin/harness-diagnostic.cjs +326 -0
- package/bin/hazard-model.cjs +261 -0
- package/bin/install-formal-tools.cjs +1 -1
- package/bin/install.js +184 -139
- package/bin/instrumentation-map.cjs +178 -0
- package/bin/invariant-catalog.cjs +437 -0
- package/bin/issue-classifier.cjs +2 -2
- package/bin/load-baseline-requirements.cjs +4 -4
- package/bin/manage-agents-core.cjs +32 -32
- package/bin/migrate-to-slots.cjs +39 -39
- package/bin/mismatch-register.cjs +217 -0
- package/bin/nForma.cjs +176 -81
- package/bin/{qgsd-solve.cjs → nf-solve.cjs} +327 -14
- package/bin/observe-config.cjs +8 -0
- package/bin/observe-debt-writer.cjs +1 -1
- package/bin/observe-handler-deps.cjs +356 -0
- package/bin/observe-handler-grafana.cjs +2 -17
- package/bin/observe-handler-internal.cjs +5 -5
- package/bin/observe-handler-logstash.cjs +2 -17
- package/bin/observe-handler-prometheus.cjs +2 -17
- package/bin/observe-handler-upstream.cjs +251 -0
- package/bin/observe-handlers.cjs +12 -33
- package/bin/observe-render.cjs +68 -22
- package/bin/observe-utils.cjs +37 -0
- package/bin/observed-fsm.cjs +324 -0
- package/bin/planning-paths.cjs +6 -0
- package/bin/polyrepo.cjs +1 -1
- package/bin/probe-quorum-slots.cjs +1 -1
- package/bin/promote-gate-maturity.cjs +274 -0
- package/bin/promote-model.cjs +1 -1
- package/bin/propose-debug-invariants.cjs +1 -1
- package/bin/quorum-cache.cjs +144 -0
- package/bin/quorum-consensus-gate.cjs +1 -1
- package/bin/quorum-preflight.cjs +89 -0
- package/bin/quorum-slot-dispatch.cjs +6 -6
- package/bin/requirements-core.cjs +1 -1
- package/bin/review-mcp-logs.cjs +1 -1
- package/bin/risk-heatmap.cjs +151 -0
- package/bin/run-account-manager-tlc.cjs +4 -4
- package/bin/run-account-pool-alloy.cjs +2 -2
- package/bin/run-alloy.cjs +2 -2
- package/bin/run-audit-alloy.cjs +2 -2
- package/bin/run-breaker-tlc.cjs +3 -3
- package/bin/run-formal-check.cjs +9 -9
- package/bin/run-formal-verify.cjs +30 -9
- package/bin/run-installer-alloy.cjs +2 -2
- package/bin/run-oscillation-tlc.cjs +4 -4
- package/bin/run-phase-tlc.cjs +1 -1
- package/bin/run-protocol-tlc.cjs +4 -4
- package/bin/run-quorum-composition-alloy.cjs +2 -2
- package/bin/run-sensitivity-sweep.cjs +2 -2
- package/bin/run-stop-hook-tlc.cjs +3 -3
- package/bin/run-tlc.cjs +21 -21
- package/bin/run-transcript-alloy.cjs +2 -2
- package/bin/secrets.cjs +5 -5
- package/bin/security-sweep.cjs +238 -0
- package/bin/sensitivity-report.cjs +3 -3
- package/bin/set-secret.cjs +5 -5
- package/bin/setup-telemetry-cron.sh +3 -3
- package/bin/stall-detector.cjs +126 -0
- package/bin/state-candidates.cjs +206 -0
- package/bin/sync-baseline-requirements.cjs +1 -1
- package/bin/telemetry-collector.cjs +1 -1
- package/bin/test-changed.cjs +111 -0
- package/bin/test-recipe-gen.cjs +250 -0
- package/bin/trace-corpus-stats.cjs +211 -0
- package/bin/unified-mcp-server.mjs +3 -3
- package/bin/update-scoreboard.cjs +1 -1
- package/bin/validate-memory.cjs +2 -2
- package/bin/validate-traces.cjs +10 -10
- package/bin/verify-quorum-health.cjs +66 -5
- package/bin/xstate-to-tla.cjs +4 -4
- package/bin/xstate-trace-walker.cjs +3 -3
- package/commands/{qgsd → nf}/add-phase.md +3 -3
- package/commands/{qgsd → nf}/add-requirement.md +3 -3
- package/commands/{qgsd → nf}/add-todo.md +3 -3
- package/commands/{qgsd → nf}/audit-milestone.md +4 -4
- package/commands/{qgsd → nf}/check-todos.md +3 -3
- package/commands/{qgsd → nf}/cleanup.md +3 -3
- package/commands/{qgsd → nf}/close-formal-gaps.md +2 -2
- package/commands/{qgsd → nf}/complete-milestone.md +9 -9
- package/commands/{qgsd → nf}/debug.md +9 -9
- package/commands/{qgsd → nf}/discuss-phase.md +3 -3
- package/commands/{qgsd → nf}/execute-phase.md +15 -15
- package/commands/{qgsd → nf}/fix-tests.md +3 -3
- package/commands/{qgsd → nf}/formal-test-sync.md +1 -1
- package/commands/{qgsd → nf}/health.md +3 -3
- package/commands/{qgsd → nf}/help.md +3 -3
- package/commands/{qgsd → nf}/insert-phase.md +3 -3
- package/commands/nf/join-discord.md +18 -0
- package/commands/{qgsd → nf}/list-phase-assumptions.md +2 -2
- package/commands/{qgsd → nf}/map-codebase.md +7 -7
- package/commands/{qgsd → nf}/map-requirements.md +3 -3
- package/commands/{qgsd → nf}/mcp-restart.md +3 -3
- package/commands/{qgsd → nf}/mcp-set-model.md +8 -8
- package/commands/{qgsd → nf}/mcp-setup.md +63 -63
- package/commands/{qgsd → nf}/mcp-status.md +3 -3
- package/commands/{qgsd → nf}/mcp-update.md +7 -7
- package/commands/{qgsd → nf}/new-milestone.md +8 -8
- package/commands/{qgsd → nf}/new-project.md +8 -8
- package/commands/{qgsd → nf}/observe.md +49 -16
- package/commands/{qgsd → nf}/pause-work.md +3 -3
- package/commands/{qgsd → nf}/plan-milestone-gaps.md +5 -5
- package/commands/{qgsd → nf}/plan-phase.md +6 -6
- package/commands/{qgsd → nf}/polyrepo.md +2 -2
- package/commands/{qgsd → nf}/progress.md +3 -3
- package/commands/{qgsd → nf}/queue.md +2 -2
- package/commands/{qgsd → nf}/quick.md +8 -8
- package/commands/{qgsd → nf}/quorum-test.md +10 -10
- package/commands/{qgsd → nf}/quorum.md +36 -86
- package/commands/{qgsd → nf}/reapply-patches.md +2 -2
- package/commands/{qgsd → nf}/remove-phase.md +3 -3
- package/commands/{qgsd → nf}/research-phase.md +12 -12
- package/commands/{qgsd → nf}/resume-work.md +3 -3
- package/commands/nf/review-requirements.md +31 -0
- package/commands/{qgsd → nf}/set-profile.md +3 -3
- package/commands/{qgsd → nf}/settings.md +6 -6
- package/commands/{qgsd → nf}/solve.md +35 -35
- package/commands/{qgsd → nf}/sync-baselines.md +4 -4
- package/commands/{qgsd → nf}/triage.md +10 -10
- package/commands/{qgsd → nf}/update.md +3 -3
- package/commands/{qgsd → nf}/verify-work.md +5 -5
- package/hooks/dist/config-loader.js +188 -32
- package/hooks/dist/conformance-schema.cjs +2 -2
- package/hooks/dist/gsd-context-monitor.js +118 -13
- package/hooks/dist/{qgsd-check-update.js → nf-check-update.js} +5 -5
- package/hooks/dist/{qgsd-circuit-breaker.js → nf-circuit-breaker.js} +35 -24
- package/hooks/dist/{qgsd-precompact.js → nf-precompact.js} +13 -13
- package/hooks/dist/{qgsd-prompt.js → nf-prompt.js} +110 -33
- package/hooks/dist/nf-session-start.js +185 -0
- package/hooks/dist/{qgsd-slot-correlator.js → nf-slot-correlator.js} +13 -5
- package/hooks/dist/{qgsd-spec-regen.js → nf-spec-regen.js} +17 -8
- package/hooks/dist/{qgsd-statusline.js → nf-statusline.js} +12 -3
- package/hooks/dist/{qgsd-stop.js → nf-stop.js} +152 -18
- package/hooks/dist/{qgsd-token-collector.js → nf-token-collector.js} +12 -4
- package/hooks/dist/unified-mcp-server.mjs +2 -2
- package/package.json +6 -4
- package/scripts/build-hooks.js +13 -6
- package/scripts/secret-audit.sh +1 -1
- package/scripts/verify-hooks-sync.cjs +90 -0
- package/templates/{qgsd.json → nf.json} +4 -4
- package/commands/qgsd/join-discord.md +0 -18
- package/hooks/dist/qgsd-session-start.js +0 -122
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Pure functions and shared helpers for manage-agents CLI and blessed TUI.
|
|
5
5
|
* No interactive dependencies (inquirer, blessed). This module provides:
|
|
6
6
|
* - File I/O helpers: readClaudeJson, writeClaudeJson, readProvidersJson, writeProvidersJson
|
|
7
|
-
* - Configuration parsing:
|
|
7
|
+
* - Configuration parsing: readNfJson, readCcrConfigSafe, getGlobalMcpServers
|
|
8
8
|
* - Provider management: fetchProviderModels, probeProviderUrl, probeAllSlots, liveDashboard
|
|
9
9
|
* - Pure transformation functions exported in _pure namespace
|
|
10
10
|
*
|
|
@@ -23,9 +23,9 @@ const { updateAgents, getUpdateStatuses } = require('./update-agents.cjs');
|
|
|
23
23
|
// File paths
|
|
24
24
|
const CLAUDE_JSON_PATH = path.join(os.homedir(), '.claude.json');
|
|
25
25
|
const CLAUDE_JSON_TMP = CLAUDE_JSON_PATH + '.tmp';
|
|
26
|
-
const
|
|
26
|
+
const NF_JSON_PATH = path.join(os.homedir(), '.claude', 'nf.json');
|
|
27
27
|
const CCR_CONFIG_PATH = path.join(os.homedir(), '.claude-code-router', 'config.json');
|
|
28
|
-
const UPDATE_LOG_PATH = path.join(os.homedir(), '.claude', '
|
|
28
|
+
const UPDATE_LOG_PATH = path.join(os.homedir(), '.claude', 'nf-update.log');
|
|
29
29
|
const PROVIDERS_JSON_PATH = path.join(__dirname, 'providers.json');
|
|
30
30
|
const PROVIDERS_JSON_TMP = PROVIDERS_JSON_PATH + '.tmp';
|
|
31
31
|
|
|
@@ -145,7 +145,7 @@ function probeProviderUrl(baseUrl, apiKey) {
|
|
|
145
145
|
headers: {
|
|
146
146
|
'Authorization': apiKey ? `Bearer ${apiKey}` : '',
|
|
147
147
|
'Accept': 'application/json',
|
|
148
|
-
'User-Agent': '
|
|
148
|
+
'User-Agent': 'nf-manage-agents/1.0',
|
|
149
149
|
},
|
|
150
150
|
timeout: 7000,
|
|
151
151
|
},
|
|
@@ -185,17 +185,17 @@ function classifyProbeResult(probeResult) {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
188
|
-
* Write key validity status to
|
|
188
|
+
* Write key validity status to nf.json under agent_config[slotName].key_status.
|
|
189
189
|
*/
|
|
190
190
|
function writeKeyStatus(slotName, status, filePath) {
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
193
|
-
if (!
|
|
194
|
-
|
|
191
|
+
const nfCfg = readNfJson(filePath);
|
|
192
|
+
if (!nf.agent_config) nf.agent_config = {};
|
|
193
|
+
if (!nf.agent_config[slotName]) nf.agent_config[slotName] = {};
|
|
194
|
+
nf.agent_config[slotName].key_status = {
|
|
195
195
|
status,
|
|
196
196
|
checkedAt: new Date().toISOString(),
|
|
197
197
|
};
|
|
198
|
-
|
|
198
|
+
writeNfJson(nfCfg, filePath);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
/**
|
|
@@ -214,7 +214,7 @@ function formatTimestamp(ts) {
|
|
|
214
214
|
*/
|
|
215
215
|
function buildDashboardLines(slots, mcpServers, healthMap, lastUpdated) {
|
|
216
216
|
const lines = [];
|
|
217
|
-
lines.push('
|
|
217
|
+
lines.push(' nForma Live Health Dashboard');
|
|
218
218
|
lines.push(' ' + '\u2500'.repeat(60));
|
|
219
219
|
lines.push('');
|
|
220
220
|
|
|
@@ -324,11 +324,11 @@ function writeProvidersJson(data) {
|
|
|
324
324
|
}
|
|
325
325
|
|
|
326
326
|
// ---------------------------------------------------------------------------
|
|
327
|
-
//
|
|
327
|
+
// nForma JSON helpers
|
|
328
328
|
// ---------------------------------------------------------------------------
|
|
329
329
|
|
|
330
|
-
function
|
|
331
|
-
const p = filePath ||
|
|
330
|
+
function readNfJson(filePath) {
|
|
331
|
+
const p = filePath || NF_JSON_PATH;
|
|
332
332
|
if (!fs.existsSync(p)) return {};
|
|
333
333
|
try {
|
|
334
334
|
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
@@ -337,19 +337,19 @@ function readQgsdJson(filePath) {
|
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
function
|
|
341
|
-
const p = filePath ||
|
|
340
|
+
function writeNfJson(data, filePath) {
|
|
341
|
+
const p = filePath || NF_JSON_PATH;
|
|
342
342
|
const tmp = p + '.tmp';
|
|
343
343
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
|
|
344
344
|
fs.renameSync(tmp, p);
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
function writeUpdatePolicy(slotName, policy, filePath) {
|
|
348
|
-
const
|
|
349
|
-
if (!
|
|
350
|
-
if (!
|
|
351
|
-
|
|
352
|
-
|
|
348
|
+
const nfCfg = readNfJson(filePath);
|
|
349
|
+
if (!nf.agent_config) nf.agent_config = {};
|
|
350
|
+
if (!nf.agent_config[slotName]) nf.agent_config[slotName] = {};
|
|
351
|
+
nf.agent_config[slotName].update_policy = policy;
|
|
352
|
+
writeNfJson(nfCfg, filePath);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
// ---------------------------------------------------------------------------
|
|
@@ -392,11 +392,11 @@ function applyKeyUpdate(updates, keytarAccount, newEnv, secretsLib) {
|
|
|
392
392
|
if (!('apiKey' in updates)) return newEnv;
|
|
393
393
|
if (updates.apiKey === '__REMOVE__') {
|
|
394
394
|
delete newEnv.ANTHROPIC_API_KEY;
|
|
395
|
-
if (secretsLib) secretsLib.delete('
|
|
395
|
+
if (secretsLib) secretsLib.delete('nforma', keytarAccount);
|
|
396
396
|
} else {
|
|
397
397
|
delete newEnv.ANTHROPIC_API_KEY;
|
|
398
398
|
if (secretsLib) {
|
|
399
|
-
secretsLib.set('
|
|
399
|
+
secretsLib.set('nforma', keytarAccount, updates.apiKey);
|
|
400
400
|
} else {
|
|
401
401
|
newEnv.ANTHROPIC_API_KEY = updates.apiKey;
|
|
402
402
|
}
|
|
@@ -406,11 +406,11 @@ function applyKeyUpdate(updates, keytarAccount, newEnv, secretsLib) {
|
|
|
406
406
|
|
|
407
407
|
async function applyCcrProviderUpdate(subAction, selectedKey, keyValue, secretsLib) {
|
|
408
408
|
if (subAction === 'set') {
|
|
409
|
-
await secretsLib.set('
|
|
409
|
+
await secretsLib.set('nforma', selectedKey, keyValue);
|
|
410
410
|
return { action: 'set', key: selectedKey };
|
|
411
411
|
}
|
|
412
412
|
if (subAction === 'remove') {
|
|
413
|
-
await secretsLib.delete('
|
|
413
|
+
await secretsLib.delete('nforma', selectedKey);
|
|
414
414
|
return { action: 'remove', key: selectedKey };
|
|
415
415
|
}
|
|
416
416
|
return null;
|
|
@@ -611,7 +611,7 @@ function validateImportSchema(parsed) {
|
|
|
611
611
|
// ---------------------------------------------------------------------------
|
|
612
612
|
|
|
613
613
|
/**
|
|
614
|
-
* Probe a single slot and persist key_status to
|
|
614
|
+
* Probe a single slot and persist key_status to nf.json if the provider responded.
|
|
615
615
|
* Does NOT persist for timeout/network errors (statusCode is null) -- this is
|
|
616
616
|
* a provider/network issue, not a key validity issue.
|
|
617
617
|
* @param {string} slotName - MCP server slot name (e.g. 'claude-1')
|
|
@@ -642,7 +642,7 @@ async function probeAllSlots(mcpServers, slots, secretsLib) {
|
|
|
642
642
|
let apiKey = env.ANTHROPIC_API_KEY || '';
|
|
643
643
|
if (secretsLib) {
|
|
644
644
|
try {
|
|
645
|
-
const k = await secretsLib.get('
|
|
645
|
+
const k = await secretsLib.get('nforma', account);
|
|
646
646
|
if (k) apiKey = k;
|
|
647
647
|
} catch (_) {}
|
|
648
648
|
}
|
|
@@ -654,9 +654,9 @@ async function probeAllSlots(mcpServers, slots, secretsLib) {
|
|
|
654
654
|
|
|
655
655
|
async function runAutoUpdateCheck(getStatusesFn = getUpdateStatuses) {
|
|
656
656
|
const check = async () => {
|
|
657
|
-
let
|
|
658
|
-
try {
|
|
659
|
-
const agentConfig =
|
|
657
|
+
let nfCfg;
|
|
658
|
+
try { nfCfg = readNfJson(); } catch { return; }
|
|
659
|
+
const agentConfig = nf.agent_config || {};
|
|
660
660
|
const autoSlots = Object.keys(agentConfig).filter(
|
|
661
661
|
(s) => agentConfig[s] && agentConfig[s].update_policy === 'auto'
|
|
662
662
|
);
|
|
@@ -777,8 +777,8 @@ module.exports._pure = {
|
|
|
777
777
|
buildAgentChoiceLabel,
|
|
778
778
|
applyKeyUpdate,
|
|
779
779
|
applyCcrProviderUpdate,
|
|
780
|
-
|
|
781
|
-
|
|
780
|
+
readNfJson,
|
|
781
|
+
writeNfJson,
|
|
782
782
|
slotToFamily,
|
|
783
783
|
getWlDisplay,
|
|
784
784
|
readCcrConfigSafe,
|
package/bin/migrate-to-slots.cjs
CHANGED
|
@@ -62,8 +62,8 @@ function migrateClaudeJson(claudeJsonPath, dryRun = false) {
|
|
|
62
62
|
return { changed, renamed };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
// tool_prefix migration map for
|
|
66
|
-
const
|
|
65
|
+
// tool_prefix migration map for nf.json
|
|
66
|
+
const NF_PREFIX_MAP = {
|
|
67
67
|
'mcp__codex-cli__': 'mcp__codex-cli-1__',
|
|
68
68
|
'mcp__gemini-cli__': 'mcp__gemini-cli-1__',
|
|
69
69
|
'mcp__opencode__': 'mcp__opencode-1__',
|
|
@@ -71,21 +71,21 @@ const QGSD_PREFIX_MAP = {
|
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* Migrate ~/.claude/
|
|
75
|
-
* @param {string}
|
|
74
|
+
* Migrate ~/.claude/nf.json required_models tool_prefix values to slot-based prefixes.
|
|
75
|
+
* @param {string} nfJsonPath - Absolute path to ~/.claude/nf.json
|
|
76
76
|
* @param {boolean} dryRun - If true, do not write changes
|
|
77
77
|
* @returns {{ changed: number, patched: Array<{key: string, from: string, to: string}> }}
|
|
78
78
|
*/
|
|
79
|
-
function
|
|
80
|
-
if (!fs.existsSync(
|
|
79
|
+
function migrateNfJson(nfJsonPath, dryRun = false) {
|
|
80
|
+
if (!fs.existsSync(nfJsonPath)) {
|
|
81
81
|
return { changed: 0, patched: [] };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
let raw;
|
|
85
85
|
try {
|
|
86
|
-
raw = JSON.parse(fs.readFileSync(
|
|
86
|
+
raw = JSON.parse(fs.readFileSync(nfJsonPath, 'utf8'));
|
|
87
87
|
} catch (e) {
|
|
88
|
-
throw new Error(`Failed to read ${
|
|
88
|
+
throw new Error(`Failed to read ${nfJsonPath}: ${e.message}`);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const requiredModels = raw.required_models;
|
|
@@ -98,7 +98,7 @@ function migrateQgsdJson(qgsdJsonPath, dryRun = false) {
|
|
|
98
98
|
|
|
99
99
|
for (const [modelKey, modelDef] of Object.entries(requiredModels)) {
|
|
100
100
|
if (modelDef && typeof modelDef.tool_prefix === 'string') {
|
|
101
|
-
const newPrefix =
|
|
101
|
+
const newPrefix = NF_PREFIX_MAP[modelDef.tool_prefix];
|
|
102
102
|
if (newPrefix) {
|
|
103
103
|
patched.push({ key: modelKey, from: modelDef.tool_prefix, to: newPrefix });
|
|
104
104
|
modelDef.tool_prefix = newPrefix;
|
|
@@ -108,21 +108,21 @@ function migrateQgsdJson(qgsdJsonPath, dryRun = false) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (changed > 0 && !dryRun) {
|
|
111
|
-
fs.writeFileSync(
|
|
111
|
+
fs.writeFileSync(nfJsonPath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
return { changed, patched };
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* Populate quorum_active in ~/.claude/
|
|
118
|
+
* Populate quorum_active in ~/.claude/nf.json from current mcpServers in ~/.claude.json.
|
|
119
119
|
* Idempotent: skips if quorum_active already present and non-empty.
|
|
120
|
-
* @param {string}
|
|
120
|
+
* @param {string} nfJsonPath - Absolute path to ~/.claude/nf.json
|
|
121
121
|
* @param {string} claudeJsonPath - Absolute path to ~/.claude.json
|
|
122
122
|
* @param {boolean} dryRun - If true, do not write changes
|
|
123
123
|
* @returns {{ skipped: boolean, slots: string[] }}
|
|
124
124
|
*/
|
|
125
|
-
function populateActiveSlots(
|
|
125
|
+
function populateActiveSlots(nfJsonPath, claudeJsonPath, dryRun = false) {
|
|
126
126
|
// Read current slot names from ~/.claude.json
|
|
127
127
|
let slotNames = [];
|
|
128
128
|
try {
|
|
@@ -134,25 +134,25 @@ function populateActiveSlots(qgsdJsonPath, claudeJsonPath, dryRun = false) {
|
|
|
134
134
|
return { skipped: true, slots: [] };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// Read or create
|
|
138
|
-
let
|
|
137
|
+
// Read or create nf.json
|
|
138
|
+
let nfConfig = {};
|
|
139
139
|
try {
|
|
140
|
-
|
|
140
|
+
nfConfig = JSON.parse(fs.readFileSync(nfJsonPath, 'utf8'));
|
|
141
141
|
} catch (e) {
|
|
142
|
-
if (e.code !== 'ENOENT') throw new Error(`Failed to read ${
|
|
143
|
-
//
|
|
142
|
+
if (e.code !== 'ENOENT') throw new Error(`Failed to read ${nfJsonPath}: ${e.message}`);
|
|
143
|
+
// nf.json absent — create minimal object
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
// Idempotent: skip if already set and non-empty
|
|
147
|
-
if (Array.isArray(
|
|
148
|
-
return { skipped: true, slots:
|
|
147
|
+
if (Array.isArray(nfConfig.quorum_active) && nfConfig.quorum_active.length > 0) {
|
|
148
|
+
return { skipped: true, slots: nfConfig.quorum_active };
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
// Populate and write
|
|
152
|
-
|
|
152
|
+
nfConfig.quorum_active = slotNames;
|
|
153
153
|
if (!dryRun) {
|
|
154
|
-
fs.mkdirSync(path.dirname(
|
|
155
|
-
fs.writeFileSync(
|
|
154
|
+
fs.mkdirSync(path.dirname(nfJsonPath), { recursive: true });
|
|
155
|
+
fs.writeFileSync(nfJsonPath, JSON.stringify(nfConfig, null, 2) + '\n', 'utf8');
|
|
156
156
|
}
|
|
157
157
|
return { skipped: false, slots: slotNames };
|
|
158
158
|
}
|
|
@@ -161,7 +161,7 @@ function populateActiveSlots(qgsdJsonPath, claudeJsonPath, dryRun = false) {
|
|
|
161
161
|
if (require.main === module) {
|
|
162
162
|
const dryRun = process.argv.includes('--dry-run');
|
|
163
163
|
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
164
|
-
const
|
|
164
|
+
const nfJsonPath = path.join(os.homedir(), '.claude', 'nf.json');
|
|
165
165
|
|
|
166
166
|
let r1, r2;
|
|
167
167
|
try {
|
|
@@ -172,15 +172,15 @@ if (require.main === module) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
try {
|
|
175
|
-
r2 =
|
|
175
|
+
r2 = migrateNfJson(nfJsonPath, dryRun);
|
|
176
176
|
} catch (e) {
|
|
177
|
-
console.error(`Error migrating ~/.claude/
|
|
177
|
+
console.error(`Error migrating ~/.claude/nf.json: ${e.message}`);
|
|
178
178
|
process.exit(1);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
let r3;
|
|
182
182
|
try {
|
|
183
|
-
r3 = populateActiveSlots(
|
|
183
|
+
r3 = populateActiveSlots(nfJsonPath, claudeJsonPath, dryRun);
|
|
184
184
|
if (!r3.skipped) {
|
|
185
185
|
console.log(`[migrate-to-slots] quorum_active populated: ${r3.slots.join(', ')}`);
|
|
186
186
|
} else {
|
|
@@ -200,7 +200,7 @@ if (require.main === module) {
|
|
|
200
200
|
console.log(` mcpServers: ${from} → ${to}`);
|
|
201
201
|
}
|
|
202
202
|
for (const { key, from, to } of r2.patched) {
|
|
203
|
-
console.log(`
|
|
203
|
+
console.log(` nf.json required_models.${key}.tool_prefix: ${from} → ${to}`);
|
|
204
204
|
}
|
|
205
205
|
} else {
|
|
206
206
|
if (r1.changed > 0) {
|
|
@@ -210,7 +210,7 @@ if (require.main === module) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
if (r2.changed > 0) {
|
|
213
|
-
console.log(`Patched ${r2.changed}
|
|
213
|
+
console.log(`Patched ${r2.changed} nf.json tool_prefix values`);
|
|
214
214
|
for (const { key, from, to } of r2.patched) {
|
|
215
215
|
console.log(` required_models.${key}.tool_prefix: ${from} → ${to}`);
|
|
216
216
|
}
|
|
@@ -221,24 +221,24 @@ if (require.main === module) {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
|
-
* Append a single slot name to quorum_active in
|
|
224
|
+
* Append a single slot name to quorum_active in nf.json if not already present.
|
|
225
225
|
* Idempotent: no-op if slot is already in the array.
|
|
226
226
|
* @param {string} slotName - e.g. "copilot-2"
|
|
227
|
-
* @param {string}
|
|
227
|
+
* @param {string} nfJsonPath - path to ~/.claude/nf.json
|
|
228
228
|
* @param {boolean} dryRun - if true, report but do not write
|
|
229
229
|
* @returns {{ added: boolean, slot: string, skipped: boolean }}
|
|
230
230
|
*/
|
|
231
|
-
function addSlotToQuorumActive(slotName,
|
|
232
|
-
let
|
|
231
|
+
function addSlotToQuorumActive(slotName, nfJsonPath, dryRun = false) {
|
|
232
|
+
let nfConfig = {};
|
|
233
233
|
try {
|
|
234
|
-
if (fs.existsSync(
|
|
235
|
-
|
|
234
|
+
if (fs.existsSync(nfJsonPath)) {
|
|
235
|
+
nfConfig = JSON.parse(fs.readFileSync(nfJsonPath, 'utf8'));
|
|
236
236
|
}
|
|
237
237
|
} catch (e) {
|
|
238
238
|
return { added: false, slot: slotName, skipped: true, error: e.message };
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
const active = Array.isArray(
|
|
241
|
+
const active = Array.isArray(nfConfig.quorum_active) ? nfConfig.quorum_active : [];
|
|
242
242
|
if (active.includes(slotName)) {
|
|
243
243
|
return { added: false, slot: slotName, skipped: true, reason: 'already present' };
|
|
244
244
|
}
|
|
@@ -247,9 +247,9 @@ function addSlotToQuorumActive(slotName, qgsdJsonPath, dryRun = false) {
|
|
|
247
247
|
return { added: true, slot: slotName, skipped: false, dryRun: true };
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
|
|
251
|
-
fs.writeFileSync(
|
|
250
|
+
nfConfig.quorum_active = [...active, slotName];
|
|
251
|
+
fs.writeFileSync(nfJsonPath, JSON.stringify(nfConfig, null, 2) + '\n');
|
|
252
252
|
return { added: true, slot: slotName, skipped: false };
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
module.exports = { migrateClaudeJson,
|
|
255
|
+
module.exports = { migrateClaudeJson, migrateNfJson, populateActiveSlots, addSlotToQuorumActive, SLOT_MIGRATION_MAP };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// bin/mismatch-register.cjs
|
|
4
|
+
// Builds a JSONL mismatch register tracking L2-model vs L1-trace disagreements.
|
|
5
|
+
// Each line is a standalone JSON object with resolution tracking.
|
|
6
|
+
// Requirements: SEM-02
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
12
|
+
|
|
13
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Classify a conformance event as a methodology skip.
|
|
17
|
+
* H1 methodology: mid-session events (phase !== 'IDLE' AND action !== 'quorum_start')
|
|
18
|
+
* cannot be validated with a fresh actor from IDLE, so they are methodology skips,
|
|
19
|
+
* NOT mismatches. Fresh actors always start in IDLE, so mid-session events would
|
|
20
|
+
* produce false mismatches without this compensation.
|
|
21
|
+
*/
|
|
22
|
+
function isMethodologySkip(event) {
|
|
23
|
+
if (!event) return false;
|
|
24
|
+
// quorum_start always starts from IDLE -- valid for fresh-actor replay
|
|
25
|
+
if (event.action === 'quorum_start') return false;
|
|
26
|
+
// Events with non-IDLE phase are mid-session
|
|
27
|
+
if (event.phase && event.phase !== 'IDLE') return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a mismatch entry from a conformance event comparison.
|
|
33
|
+
*/
|
|
34
|
+
function buildMismatchEntry(id, event, eventIndex, expectedState, actualState, divergenceType, resolution, notes, classification) {
|
|
35
|
+
return {
|
|
36
|
+
id: 'MISMATCH-' + String(id).padStart(3, '0'),
|
|
37
|
+
timestamp: event.ts || new Date().toISOString(),
|
|
38
|
+
l2_source: 'nf-workflow.machine.ts',
|
|
39
|
+
l1_trace_ref: {
|
|
40
|
+
event_index: eventIndex,
|
|
41
|
+
session: event.session_id || event.round_id || 'standalone',
|
|
42
|
+
},
|
|
43
|
+
expected_state: expectedState,
|
|
44
|
+
actual_state: actualState,
|
|
45
|
+
divergence_type: divergenceType,
|
|
46
|
+
resolution: resolution || 'open',
|
|
47
|
+
resolution_notes: notes || null,
|
|
48
|
+
classification: classification || null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Process conformance events and build mismatch entries.
|
|
54
|
+
* Returns { entries, summary }.
|
|
55
|
+
*/
|
|
56
|
+
function buildMismatchRegister(conformanceEvents, vocabulary, divergences) {
|
|
57
|
+
// Lazy-load validate-traces for mapToXStateEvent
|
|
58
|
+
const { mapToXStateEvent } = require(path.join(__dirname, 'validate-traces.cjs'));
|
|
59
|
+
|
|
60
|
+
// Machine path resolution (same pattern as validate-traces.cjs)
|
|
61
|
+
const machinePath = (() => {
|
|
62
|
+
const repoDist = path.join(__dirname, '..', 'dist', 'machines', 'nf-workflow.machine.js');
|
|
63
|
+
const installDist = path.join(__dirname, 'dist', 'machines', 'nf-workflow.machine.js');
|
|
64
|
+
return fs.existsSync(repoDist) ? repoDist : installDist;
|
|
65
|
+
})();
|
|
66
|
+
const { createActor, nfWorkflowMachine } = require(machinePath);
|
|
67
|
+
|
|
68
|
+
const entries = [];
|
|
69
|
+
let mismatchCounter = 0;
|
|
70
|
+
const stats = { total_events: 0, mapped: 0, unmapped: 0, methodology_skip: 0, state_mismatch: 0 };
|
|
71
|
+
|
|
72
|
+
// Process conformance events
|
|
73
|
+
for (let i = 0; i < conformanceEvents.length; i++) {
|
|
74
|
+
const event = conformanceEvents[i];
|
|
75
|
+
stats.total_events++;
|
|
76
|
+
|
|
77
|
+
// Map to XState event
|
|
78
|
+
const xstateEvent = mapToXStateEvent(event);
|
|
79
|
+
if (!xstateEvent) {
|
|
80
|
+
stats.unmapped++;
|
|
81
|
+
continue; // instrumentation gap, not a mismatch
|
|
82
|
+
}
|
|
83
|
+
stats.mapped++;
|
|
84
|
+
|
|
85
|
+
// H1 methodology skip: mid-session events cannot be validated with fresh actor
|
|
86
|
+
if (isMethodologySkip(event)) {
|
|
87
|
+
stats.methodology_skip++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Fresh actor per event for isolation
|
|
92
|
+
const actor = createActor(nfWorkflowMachine);
|
|
93
|
+
actor.start();
|
|
94
|
+
actor.send(xstateEvent);
|
|
95
|
+
const snapshot = actor.getSnapshot();
|
|
96
|
+
actor.stop();
|
|
97
|
+
|
|
98
|
+
// Determine expected state based on event type
|
|
99
|
+
let expectedState = null;
|
|
100
|
+
if (event.action === 'quorum_start') expectedState = 'COLLECTING_VOTES';
|
|
101
|
+
else if (event.action === 'circuit_break') expectedState = 'IDLE';
|
|
102
|
+
else if (event.action === 'deliberation_round') expectedState = 'DELIBERATING';
|
|
103
|
+
else if (event.outcome === 'APPROVE' || event.outcome === 'BLOCK') expectedState = 'DECIDED';
|
|
104
|
+
|
|
105
|
+
if (expectedState === null) continue; // cannot determine -- skip
|
|
106
|
+
|
|
107
|
+
// Get actual state as string
|
|
108
|
+
const actualStateValue = typeof snapshot.value === 'string'
|
|
109
|
+
? snapshot.value
|
|
110
|
+
: JSON.stringify(snapshot.value);
|
|
111
|
+
|
|
112
|
+
// Compare states
|
|
113
|
+
if (!snapshot.matches(expectedState)) {
|
|
114
|
+
mismatchCounter++;
|
|
115
|
+
stats.state_mismatch++;
|
|
116
|
+
entries.push(buildMismatchEntry(
|
|
117
|
+
mismatchCounter, event, i,
|
|
118
|
+
expectedState, actualStateValue,
|
|
119
|
+
'state_mismatch', 'open', null, null
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Incorporate existing .divergences.json entries
|
|
125
|
+
if (Array.isArray(divergences)) {
|
|
126
|
+
for (const div of divergences) {
|
|
127
|
+
mismatchCounter++;
|
|
128
|
+
const divEvent = div.event || {};
|
|
129
|
+
|
|
130
|
+
// Check if this divergence matches the H1 methodology skip pattern
|
|
131
|
+
const isH1 = isMethodologySkip(divEvent);
|
|
132
|
+
const resolution = isH1 ? 'explained' : 'open';
|
|
133
|
+
const notes = isH1 ? 'H1 methodology limitation: mid-session event evaluated with fresh actor from IDLE' : null;
|
|
134
|
+
|
|
135
|
+
entries.push(buildMismatchEntry(
|
|
136
|
+
mismatchCounter, divEvent, divEvent._lineIndex || 0,
|
|
137
|
+
div.expectedState || 'unknown',
|
|
138
|
+
typeof div.actualState === 'string' ? div.actualState : JSON.stringify(div.actualState || 'unknown'),
|
|
139
|
+
div.divergenceType || 'state_mismatch',
|
|
140
|
+
resolution, notes,
|
|
141
|
+
isH1 ? 'methodology_limitation' : null
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const byResolution = { open: 0, explained: 0, bug: 0 };
|
|
147
|
+
const byDivergenceType = {};
|
|
148
|
+
for (const e of entries) {
|
|
149
|
+
byResolution[e.resolution] = (byResolution[e.resolution] || 0) + 1;
|
|
150
|
+
byDivergenceType[e.divergence_type] = (byDivergenceType[e.divergence_type] || 0) + 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
entries,
|
|
155
|
+
summary: {
|
|
156
|
+
total_mismatches: entries.length,
|
|
157
|
+
by_resolution: byResolution,
|
|
158
|
+
by_divergence_type: byDivergenceType,
|
|
159
|
+
stats,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
if (require.main === module) {
|
|
167
|
+
const jsonFlag = process.argv.includes('--json');
|
|
168
|
+
|
|
169
|
+
// Read conformance events
|
|
170
|
+
const pp = require(path.join(__dirname, 'planning-paths.cjs'));
|
|
171
|
+
const logPath = pp.resolveWithFallback(PROJECT_ROOT, 'conformance-events');
|
|
172
|
+
let conformanceEvents = [];
|
|
173
|
+
if (fs.existsSync(logPath)) {
|
|
174
|
+
const raw = fs.readFileSync(logPath, 'utf8');
|
|
175
|
+
const lines = raw.split('\n').filter(l => l.trim().length > 0);
|
|
176
|
+
for (const line of lines) {
|
|
177
|
+
try { conformanceEvents.push(JSON.parse(line)); } catch (_) { /* skip malformed lines */ }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Read event vocabulary
|
|
182
|
+
const vocabPath = path.join(PROJECT_ROOT, '.planning', 'formal', 'evidence', 'event-vocabulary.json');
|
|
183
|
+
let vocabulary = {};
|
|
184
|
+
if (fs.existsSync(vocabPath)) {
|
|
185
|
+
try { vocabulary = JSON.parse(fs.readFileSync(vocabPath, 'utf8')); } catch (_) { /* fail-open */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Read existing divergences
|
|
189
|
+
const divergencesPath = path.join(PROJECT_ROOT, '.planning', 'formal', '.divergences.json');
|
|
190
|
+
let divergences = [];
|
|
191
|
+
if (fs.existsSync(divergencesPath)) {
|
|
192
|
+
try { divergences = JSON.parse(fs.readFileSync(divergencesPath, 'utf8')); } catch (_) { /* fail-open */ }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = buildMismatchRegister(conformanceEvents, vocabulary, divergences);
|
|
196
|
+
|
|
197
|
+
// Write JSONL output (one JSON object per line)
|
|
198
|
+
const outputPath = path.join(PROJECT_ROOT, '.planning', 'formal', 'semantics', 'mismatch-register.jsonl');
|
|
199
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
200
|
+
const jsonlContent = result.entries.map(e => JSON.stringify(e)).join('\n');
|
|
201
|
+
fs.writeFileSync(outputPath, jsonlContent + (jsonlContent.length > 0 ? '\n' : ''), 'utf8');
|
|
202
|
+
|
|
203
|
+
// Output summary
|
|
204
|
+
if (jsonFlag) {
|
|
205
|
+
process.stdout.write(JSON.stringify(result.summary, null, 2) + '\n');
|
|
206
|
+
} else {
|
|
207
|
+
process.stdout.write('[mismatch-register] Total mismatches: ' + result.summary.total_mismatches + '\n');
|
|
208
|
+
process.stdout.write('[mismatch-register] By resolution: ' + JSON.stringify(result.summary.by_resolution) + '\n');
|
|
209
|
+
process.stdout.write('[mismatch-register] By type: ' + JSON.stringify(result.summary.by_divergence_type) + '\n');
|
|
210
|
+
process.stdout.write('[mismatch-register] Stats: ' + JSON.stringify(result.summary.stats) + '\n');
|
|
211
|
+
process.stdout.write('[mismatch-register] Output: ' + outputPath + '\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = { buildMismatchRegister, buildMismatchEntry, isMethodologySkip };
|