@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
|
@@ -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/
|
|
3
|
-
// SubagentStart hook — writes a correlation placeholder file for
|
|
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 === '
|
|
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
|
-
//
|
|
29
|
-
|
|
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/
|
|
3
|
-
// PostToolUse hook: when Claude writes to
|
|
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
|
|
30
|
-
if (toolName !== 'Write' || !filePath.includes('
|
|
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
|
|
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', '
|
|
56
|
-
const guardsConfig = path.join(cwd, '.planning', 'formal', 'tla', 'guards', '
|
|
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=
|
|
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
|
-
//
|
|
78
|
+
// nForma update available?
|
|
70
79
|
let gsdUpdate = '';
|
|
71
|
-
const cacheFile = path.join(homeDir, '.claude', 'cache', '
|
|
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⬆ /
|
|
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/
|
|
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/
|
|
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
|
-
// (
|
|
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('[
|
|
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:<
|
|
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:<
|
|
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 '/
|
|
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 '/
|
|
157
|
+
return '/nf:plan-phase';
|
|
152
158
|
}
|
|
153
159
|
|
|
154
|
-
// Returns true if any assistant turn used Task(subagent_type=
|
|
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
|
|
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 === '
|
|
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
|
|
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.
|
|
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('[
|
|
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
|
-
|
|
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
|
|
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/
|
|
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 === '
|
|
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
|
-
//
|
|
82
|
-
|
|
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
|
|