@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
|
//
|
|
5
5
|
// Exports: loadConfig(projectDir?), DEFAULT_CONFIG
|
|
6
6
|
//
|
|
7
|
-
// Load order: DEFAULT_CONFIG → ~/.claude/
|
|
7
|
+
// Load order: DEFAULT_CONFIG → ~/.claude/nf.json (global) → .claude/nf.json in projectDir (project)
|
|
8
8
|
// Merge: shallow spread — project values fully replace global values for any overlapping key.
|
|
9
9
|
// Warnings: all written to process.stderr — stdout is never touched (it is the hook decision channel).
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ const path = require('path');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
|
|
17
17
|
// Maps the family name of a slot (trailing -N stripped) to the MCP tool suffix to call.
|
|
18
|
-
// Used by both
|
|
18
|
+
// Used by both nf-prompt.js (step generation) and nf-stop.js (evidence detection).
|
|
19
19
|
const SLOT_TOOL_SUFFIX = {
|
|
20
20
|
'codex-cli': 'review',
|
|
21
21
|
'codex': 'review',
|
|
@@ -35,6 +35,42 @@ function slotToToolCall(slotName) {
|
|
|
35
35
|
return 'mcp__' + slotName + '__' + suffix;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const HOOK_PROFILE_MAP = {
|
|
39
|
+
minimal: new Set([
|
|
40
|
+
'nf-circuit-breaker',
|
|
41
|
+
'nf-precompact',
|
|
42
|
+
]),
|
|
43
|
+
standard: new Set([
|
|
44
|
+
'nf-circuit-breaker',
|
|
45
|
+
'nf-precompact',
|
|
46
|
+
'nf-prompt',
|
|
47
|
+
'nf-stop',
|
|
48
|
+
'gsd-context-monitor',
|
|
49
|
+
'nf-spec-regen',
|
|
50
|
+
'nf-token-collector',
|
|
51
|
+
'nf-slot-correlator',
|
|
52
|
+
'nf-session-start',
|
|
53
|
+
'nf-statusline',
|
|
54
|
+
]),
|
|
55
|
+
strict: new Set([
|
|
56
|
+
'nf-circuit-breaker',
|
|
57
|
+
'nf-precompact',
|
|
58
|
+
'nf-prompt',
|
|
59
|
+
'nf-stop',
|
|
60
|
+
'gsd-context-monitor',
|
|
61
|
+
'nf-spec-regen',
|
|
62
|
+
'nf-token-collector',
|
|
63
|
+
'nf-slot-correlator',
|
|
64
|
+
'nf-session-start',
|
|
65
|
+
'nf-statusline',
|
|
66
|
+
]),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function shouldRunHook(hookBasename, profile) {
|
|
70
|
+
const validProfile = HOOK_PROFILE_MAP[profile] ? profile : 'standard';
|
|
71
|
+
return HOOK_PROFILE_MAP[validProfile].has(hookBasename);
|
|
72
|
+
}
|
|
73
|
+
|
|
38
74
|
const DEFAULT_CONFIG = {
|
|
39
75
|
quorum_commands: [
|
|
40
76
|
'plan-phase', 'new-project', 'new-milestone',
|
|
@@ -72,6 +108,20 @@ const DEFAULT_CONFIG = {
|
|
|
72
108
|
warn_pct: 70,
|
|
73
109
|
critical_pct: 90,
|
|
74
110
|
},
|
|
111
|
+
budget: {
|
|
112
|
+
session_limit_tokens: null, // null = disabled (fail-open)
|
|
113
|
+
warn_pct: 60, // inject warning at this % of session_limit_tokens
|
|
114
|
+
downgrade_pct: 85, // auto-downgrade model profile at this %
|
|
115
|
+
},
|
|
116
|
+
stall_detection: {
|
|
117
|
+
timeout_s: 90, // mark slot stalled after this many seconds
|
|
118
|
+
consecutive_threshold: 2, // require N consecutive stalled dispatches
|
|
119
|
+
check_commits: true, // only escalate if no new commits
|
|
120
|
+
},
|
|
121
|
+
smart_compact: {
|
|
122
|
+
enabled: true, // master switch
|
|
123
|
+
context_warn_pct: 60, // suggest compact above this context usage %
|
|
124
|
+
},
|
|
75
125
|
// quorum_active: array of slot names that participate in quorum.
|
|
76
126
|
// [] = all discovered slots participate (fail-open, backward compatible with pre-Phase-40 installs).
|
|
77
127
|
// A non-empty array is an explicit allowlist.
|
|
@@ -86,6 +136,7 @@ const DEFAULT_CONFIG = {
|
|
|
86
136
|
// task_envelope_enabled: master switch for task-envelope.json sidecar writes.
|
|
87
137
|
// Flat key required — nested objects lost in shallow merge.
|
|
88
138
|
task_envelope_enabled: true,
|
|
139
|
+
hook_profile: 'standard',
|
|
89
140
|
};
|
|
90
141
|
|
|
91
142
|
// Reads and parses a JSON config file.
|
|
@@ -97,7 +148,7 @@ function readConfigFile(filePath) {
|
|
|
97
148
|
try {
|
|
98
149
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
99
150
|
} catch (e) {
|
|
100
|
-
process.stderr.write('[
|
|
151
|
+
process.stderr.write('[nf] WARNING: Malformed config at ' + filePath + ': ' + e.message + '\n');
|
|
101
152
|
return null;
|
|
102
153
|
}
|
|
103
154
|
}
|
|
@@ -107,33 +158,33 @@ function readConfigFile(filePath) {
|
|
|
107
158
|
// Returns the (possibly corrected) config object.
|
|
108
159
|
function validateConfig(config) {
|
|
109
160
|
if (!Array.isArray(config.quorum_commands)) {
|
|
110
|
-
process.stderr.write('[
|
|
161
|
+
process.stderr.write('[nf] WARNING: nf.json: quorum_commands must be an array; using defaults\n');
|
|
111
162
|
config.quorum_commands = DEFAULT_CONFIG.quorum_commands;
|
|
112
163
|
}
|
|
113
164
|
|
|
114
165
|
if (typeof config.required_models !== 'object' || config.required_models === null) {
|
|
115
|
-
process.stderr.write('[
|
|
166
|
+
process.stderr.write('[nf] WARNING: nf.json: required_models must be an object; using defaults\n');
|
|
116
167
|
config.required_models = DEFAULT_CONFIG.required_models;
|
|
117
168
|
}
|
|
118
169
|
|
|
119
170
|
if (!['open', 'closed'].includes(config.fail_mode)) {
|
|
120
|
-
process.stderr.write('[
|
|
171
|
+
process.stderr.write('[nf] WARNING: nf.json: fail_mode "' + config.fail_mode + '" invalid; defaulting to "open"\n');
|
|
121
172
|
config.fail_mode = 'open';
|
|
122
173
|
}
|
|
123
174
|
|
|
124
175
|
// Validate circuit_breaker sub-object
|
|
125
176
|
if (typeof config.circuit_breaker !== 'object' || config.circuit_breaker === null) {
|
|
126
|
-
process.stderr.write('[
|
|
177
|
+
process.stderr.write('[nf] WARNING: nf.json: circuit_breaker must be an object; using defaults\n');
|
|
127
178
|
config.circuit_breaker = { ...DEFAULT_CONFIG.circuit_breaker };
|
|
128
179
|
} else {
|
|
129
180
|
// Validate oscillation_depth independently
|
|
130
181
|
if (!Number.isInteger(config.circuit_breaker.oscillation_depth) || config.circuit_breaker.oscillation_depth < 1) {
|
|
131
|
-
process.stderr.write('[
|
|
182
|
+
process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.oscillation_depth must be a positive integer; defaulting to 3\n');
|
|
132
183
|
config.circuit_breaker.oscillation_depth = 3;
|
|
133
184
|
}
|
|
134
185
|
// Validate commit_window independently
|
|
135
186
|
if (!Number.isInteger(config.circuit_breaker.commit_window) || config.circuit_breaker.commit_window < 1) {
|
|
136
|
-
process.stderr.write('[
|
|
187
|
+
process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.commit_window must be a positive integer; defaulting to 6\n');
|
|
137
188
|
config.circuit_breaker.commit_window = 6;
|
|
138
189
|
}
|
|
139
190
|
// Fill in missing sub-keys with defaults (handles partial circuit_breaker objects)
|
|
@@ -150,18 +201,18 @@ function validateConfig(config) {
|
|
|
150
201
|
config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
|
|
151
202
|
}
|
|
152
203
|
if (typeof config.circuit_breaker.haiku_reviewer !== 'boolean') {
|
|
153
|
-
process.stderr.write('[
|
|
204
|
+
process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.haiku_reviewer must be boolean; defaulting to true\n');
|
|
154
205
|
config.circuit_breaker.haiku_reviewer = true;
|
|
155
206
|
}
|
|
156
207
|
if (typeof config.circuit_breaker.haiku_model !== 'string') {
|
|
157
|
-
process.stderr.write('[
|
|
208
|
+
process.stderr.write('[nf] WARNING: nf.json: circuit_breaker.haiku_model must be a string; using default\n');
|
|
158
209
|
config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
|
|
159
210
|
}
|
|
160
211
|
}
|
|
161
212
|
|
|
162
213
|
// Validate quorum_active
|
|
163
214
|
if (!Array.isArray(config.quorum_active)) {
|
|
164
|
-
process.stderr.write('[
|
|
215
|
+
process.stderr.write('[nf] WARNING: nf.json: quorum_active must be an array; using []\n');
|
|
165
216
|
config.quorum_active = [];
|
|
166
217
|
} else {
|
|
167
218
|
config.quorum_active = config.quorum_active.filter(
|
|
@@ -174,7 +225,7 @@ function validateConfig(config) {
|
|
|
174
225
|
config.quorum = { ...DEFAULT_CONFIG.quorum };
|
|
175
226
|
} else {
|
|
176
227
|
if (!Number.isInteger(config.quorum.minSize) || config.quorum.minSize < 1) {
|
|
177
|
-
process.stderr.write('[
|
|
228
|
+
process.stderr.write('[nf] WARNING: nf.json: quorum.minSize must be a positive integer; defaulting to 4\n');
|
|
178
229
|
config.quorum.minSize = DEFAULT_CONFIG.quorum.minSize;
|
|
179
230
|
}
|
|
180
231
|
if (typeof config.quorum.preferSub !== 'boolean') {
|
|
@@ -184,15 +235,15 @@ function validateConfig(config) {
|
|
|
184
235
|
|
|
185
236
|
// Validate agent_config
|
|
186
237
|
if (typeof config.agent_config !== 'object' || config.agent_config === null || Array.isArray(config.agent_config)) {
|
|
187
|
-
process.stderr.write('[
|
|
238
|
+
process.stderr.write('[nf] WARNING: nf.json: agent_config must be an object; using {}\n');
|
|
188
239
|
config.agent_config = {};
|
|
189
240
|
} else {
|
|
190
241
|
for (const [slot, meta] of Object.entries(config.agent_config)) {
|
|
191
242
|
if (typeof meta !== 'object' || meta === null) {
|
|
192
|
-
process.stderr.write('[
|
|
243
|
+
process.stderr.write('[nf] WARNING: nf.json: agent_config.' + slot + ' must be an object; removing\n');
|
|
193
244
|
delete config.agent_config[slot];
|
|
194
245
|
} else if (meta.auth_type && !['sub', 'api'].includes(meta.auth_type)) {
|
|
195
|
-
process.stderr.write('[
|
|
246
|
+
process.stderr.write('[nf] WARNING: nf.json: agent_config.' + slot + '.auth_type must be "sub" or "api"; defaulting to "api"\n');
|
|
196
247
|
meta.auth_type = 'api';
|
|
197
248
|
}
|
|
198
249
|
}
|
|
@@ -200,13 +251,13 @@ function validateConfig(config) {
|
|
|
200
251
|
|
|
201
252
|
// Validate model_preferences
|
|
202
253
|
if (typeof config.model_preferences !== 'object' || config.model_preferences === null || Array.isArray(config.model_preferences)) {
|
|
203
|
-
process.stderr.write('[
|
|
254
|
+
process.stderr.write('[nf] WARNING: nf.json: model_preferences must be an object; using {}\n');
|
|
204
255
|
config.model_preferences = {};
|
|
205
256
|
} else {
|
|
206
257
|
// Remove invalid entries (non-string values) with a warning
|
|
207
258
|
for (const [key, val] of Object.entries(config.model_preferences)) {
|
|
208
259
|
if (typeof val !== 'string' || val.trim() === '') {
|
|
209
|
-
process.stderr.write('[
|
|
260
|
+
process.stderr.write('[nf] WARNING: nf.json: model_preferences.' + key + ' must be a non-empty string; removing\n');
|
|
210
261
|
delete config.model_preferences[key];
|
|
211
262
|
}
|
|
212
263
|
}
|
|
@@ -214,21 +265,21 @@ function validateConfig(config) {
|
|
|
214
265
|
|
|
215
266
|
// Validate context_monitor sub-object
|
|
216
267
|
if (typeof config.context_monitor !== 'object' || config.context_monitor === null) {
|
|
217
|
-
process.stderr.write('[
|
|
268
|
+
process.stderr.write('[nf] WARNING: nf.json: context_monitor must be an object; using defaults\n');
|
|
218
269
|
config.context_monitor = { ...DEFAULT_CONFIG.context_monitor };
|
|
219
270
|
} else {
|
|
220
271
|
if (!Number.isInteger(config.context_monitor.warn_pct) ||
|
|
221
272
|
config.context_monitor.warn_pct < 1 || config.context_monitor.warn_pct > 99) {
|
|
222
|
-
process.stderr.write('[
|
|
273
|
+
process.stderr.write('[nf] WARNING: nf.json: context_monitor.warn_pct must be an integer 1-99; defaulting to 70\n');
|
|
223
274
|
config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
|
|
224
275
|
}
|
|
225
276
|
if (!Number.isInteger(config.context_monitor.critical_pct) ||
|
|
226
277
|
config.context_monitor.critical_pct < 1 || config.context_monitor.critical_pct > 100) {
|
|
227
|
-
process.stderr.write('[
|
|
278
|
+
process.stderr.write('[nf] WARNING: nf.json: context_monitor.critical_pct must be an integer 1-100; defaulting to 90\n');
|
|
228
279
|
config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
|
|
229
280
|
}
|
|
230
281
|
if (config.context_monitor.warn_pct >= config.context_monitor.critical_pct) {
|
|
231
|
-
process.stderr.write('[
|
|
282
|
+
process.stderr.write('[nf] WARNING: nf.json: context_monitor.warn_pct must be less than critical_pct; resetting to defaults\n');
|
|
232
283
|
config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
|
|
233
284
|
config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
|
|
234
285
|
}
|
|
@@ -241,17 +292,113 @@ function validateConfig(config) {
|
|
|
241
292
|
}
|
|
242
293
|
}
|
|
243
294
|
|
|
295
|
+
// Validate budget sub-object
|
|
296
|
+
if (typeof config.budget !== 'object' || config.budget === null) {
|
|
297
|
+
process.stderr.write('[nf] WARNING: nf.json: budget must be an object; using defaults\n');
|
|
298
|
+
config.budget = { ...DEFAULT_CONFIG.budget };
|
|
299
|
+
} else {
|
|
300
|
+
// Validate session_limit_tokens: must be null, undefined, or integer >= 1000
|
|
301
|
+
if (config.budget.session_limit_tokens !== null && config.budget.session_limit_tokens !== undefined) {
|
|
302
|
+
if (!Number.isInteger(config.budget.session_limit_tokens) || config.budget.session_limit_tokens < 1000) {
|
|
303
|
+
process.stderr.write('[nf] WARNING: nf.json: budget.session_limit_tokens must be null or integer >= 1000; defaulting to null\n');
|
|
304
|
+
config.budget.session_limit_tokens = null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Validate warn_pct: integer 1-99, default 60
|
|
308
|
+
if (!Number.isInteger(config.budget.warn_pct) || config.budget.warn_pct < 1 || config.budget.warn_pct > 99) {
|
|
309
|
+
process.stderr.write('[nf] WARNING: nf.json: budget.warn_pct must be an integer 1-99; defaulting to 60\n');
|
|
310
|
+
config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
|
|
311
|
+
}
|
|
312
|
+
// Validate downgrade_pct: integer 1-100, default 85
|
|
313
|
+
if (!Number.isInteger(config.budget.downgrade_pct) || config.budget.downgrade_pct < 1 || config.budget.downgrade_pct > 100) {
|
|
314
|
+
process.stderr.write('[nf] WARNING: nf.json: budget.downgrade_pct must be an integer 1-100; defaulting to 85\n');
|
|
315
|
+
config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
|
|
316
|
+
}
|
|
317
|
+
// Validate warn_pct < downgrade_pct
|
|
318
|
+
if (config.budget.warn_pct >= config.budget.downgrade_pct) {
|
|
319
|
+
process.stderr.write('[nf] WARNING: nf.json: budget.warn_pct must be less than downgrade_pct; resetting to defaults\n');
|
|
320
|
+
config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
|
|
321
|
+
config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
|
|
322
|
+
}
|
|
323
|
+
// Fill missing sub-keys with defaults
|
|
324
|
+
if (config.budget.session_limit_tokens === undefined) {
|
|
325
|
+
config.budget.session_limit_tokens = DEFAULT_CONFIG.budget.session_limit_tokens;
|
|
326
|
+
}
|
|
327
|
+
if (config.budget.warn_pct === undefined) {
|
|
328
|
+
config.budget.warn_pct = DEFAULT_CONFIG.budget.warn_pct;
|
|
329
|
+
}
|
|
330
|
+
if (config.budget.downgrade_pct === undefined) {
|
|
331
|
+
config.budget.downgrade_pct = DEFAULT_CONFIG.budget.downgrade_pct;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Validate stall_detection sub-object
|
|
336
|
+
if (typeof config.stall_detection !== 'object' || config.stall_detection === null) {
|
|
337
|
+
process.stderr.write('[nf] WARNING: nf.json: stall_detection must be an object; using defaults\n');
|
|
338
|
+
config.stall_detection = { ...DEFAULT_CONFIG.stall_detection };
|
|
339
|
+
} else {
|
|
340
|
+
// Validate timeout_s: positive integer, default 90
|
|
341
|
+
if (!Number.isInteger(config.stall_detection.timeout_s) || config.stall_detection.timeout_s < 1) {
|
|
342
|
+
process.stderr.write('[nf] WARNING: nf.json: stall_detection.timeout_s must be a positive integer; defaulting to 90\n');
|
|
343
|
+
config.stall_detection.timeout_s = DEFAULT_CONFIG.stall_detection.timeout_s;
|
|
344
|
+
}
|
|
345
|
+
// Validate consecutive_threshold: positive integer >= 1, default 2
|
|
346
|
+
if (!Number.isInteger(config.stall_detection.consecutive_threshold) || config.stall_detection.consecutive_threshold < 1) {
|
|
347
|
+
process.stderr.write('[nf] WARNING: nf.json: stall_detection.consecutive_threshold must be a positive integer; defaulting to 2\n');
|
|
348
|
+
config.stall_detection.consecutive_threshold = DEFAULT_CONFIG.stall_detection.consecutive_threshold;
|
|
349
|
+
}
|
|
350
|
+
// Validate check_commits: boolean, default true
|
|
351
|
+
if (typeof config.stall_detection.check_commits !== 'boolean') {
|
|
352
|
+
process.stderr.write('[nf] WARNING: nf.json: stall_detection.check_commits must be a boolean; defaulting to true\n');
|
|
353
|
+
config.stall_detection.check_commits = DEFAULT_CONFIG.stall_detection.check_commits;
|
|
354
|
+
}
|
|
355
|
+
// Fill missing sub-keys with defaults
|
|
356
|
+
if (config.stall_detection.timeout_s === undefined) {
|
|
357
|
+
config.stall_detection.timeout_s = DEFAULT_CONFIG.stall_detection.timeout_s;
|
|
358
|
+
}
|
|
359
|
+
if (config.stall_detection.consecutive_threshold === undefined) {
|
|
360
|
+
config.stall_detection.consecutive_threshold = DEFAULT_CONFIG.stall_detection.consecutive_threshold;
|
|
361
|
+
}
|
|
362
|
+
if (config.stall_detection.check_commits === undefined) {
|
|
363
|
+
config.stall_detection.check_commits = DEFAULT_CONFIG.stall_detection.check_commits;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Validate smart_compact sub-object
|
|
368
|
+
if (typeof config.smart_compact !== 'object' || config.smart_compact === null) {
|
|
369
|
+
process.stderr.write('[nf] WARNING: nf.json: smart_compact must be an object; using defaults\n');
|
|
370
|
+
config.smart_compact = { ...DEFAULT_CONFIG.smart_compact };
|
|
371
|
+
} else {
|
|
372
|
+
// Validate enabled: boolean, default true
|
|
373
|
+
if (typeof config.smart_compact.enabled !== 'boolean') {
|
|
374
|
+
process.stderr.write('[nf] WARNING: nf.json: smart_compact.enabled must be a boolean; defaulting to true\n');
|
|
375
|
+
config.smart_compact.enabled = DEFAULT_CONFIG.smart_compact.enabled;
|
|
376
|
+
}
|
|
377
|
+
// Validate context_warn_pct: integer 1-99, default 60
|
|
378
|
+
if (!Number.isInteger(config.smart_compact.context_warn_pct) || config.smart_compact.context_warn_pct < 1 || config.smart_compact.context_warn_pct > 99) {
|
|
379
|
+
process.stderr.write('[nf] WARNING: nf.json: smart_compact.context_warn_pct must be an integer 1-99; defaulting to 60\n');
|
|
380
|
+
config.smart_compact.context_warn_pct = DEFAULT_CONFIG.smart_compact.context_warn_pct;
|
|
381
|
+
}
|
|
382
|
+
// Fill missing sub-keys with defaults
|
|
383
|
+
if (config.smart_compact.enabled === undefined) {
|
|
384
|
+
config.smart_compact.enabled = DEFAULT_CONFIG.smart_compact.enabled;
|
|
385
|
+
}
|
|
386
|
+
if (config.smart_compact.context_warn_pct === undefined) {
|
|
387
|
+
config.smart_compact.context_warn_pct = DEFAULT_CONFIG.smart_compact.context_warn_pct;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
244
391
|
// Validate model_tier_planner and model_tier_worker
|
|
245
392
|
const VALID_TIERS = ['haiku', 'sonnet', 'opus'];
|
|
246
393
|
if (config.model_tier_planner !== undefined) {
|
|
247
394
|
if (typeof config.model_tier_planner !== 'string' || !VALID_TIERS.includes(config.model_tier_planner)) {
|
|
248
|
-
process.stderr.write('[
|
|
395
|
+
process.stderr.write('[nf] WARNING: nf.json: model_tier_planner must be "haiku", "sonnet", or "opus"; removing\n');
|
|
249
396
|
delete config.model_tier_planner;
|
|
250
397
|
}
|
|
251
398
|
}
|
|
252
399
|
if (config.model_tier_worker !== undefined) {
|
|
253
400
|
if (typeof config.model_tier_worker !== 'string' || !VALID_TIERS.includes(config.model_tier_worker)) {
|
|
254
|
-
process.stderr.write('[
|
|
401
|
+
process.stderr.write('[nf] WARNING: nf.json: model_tier_worker must be "haiku", "sonnet", or "opus"; removing\n');
|
|
255
402
|
delete config.model_tier_worker;
|
|
256
403
|
}
|
|
257
404
|
}
|
|
@@ -259,32 +406,41 @@ function validateConfig(config) {
|
|
|
259
406
|
// Validate task_envelope_enabled
|
|
260
407
|
if (config.task_envelope_enabled !== undefined) {
|
|
261
408
|
if (typeof config.task_envelope_enabled !== 'boolean') {
|
|
262
|
-
process.stderr.write('[
|
|
409
|
+
process.stderr.write('[nf] WARNING: nf.json: task_envelope_enabled must be a boolean; using default true\n');
|
|
263
410
|
config.task_envelope_enabled = true;
|
|
264
411
|
}
|
|
265
412
|
}
|
|
266
413
|
|
|
414
|
+
// Validate hook_profile
|
|
415
|
+
const VALID_PROFILES = ['minimal', 'standard', 'strict'];
|
|
416
|
+
if (config.hook_profile !== undefined) {
|
|
417
|
+
if (typeof config.hook_profile !== 'string' || !VALID_PROFILES.includes(config.hook_profile)) {
|
|
418
|
+
process.stderr.write('[nf] WARNING: nf.json: hook_profile must be "minimal", "standard", or "strict"; defaulting to "standard"\n');
|
|
419
|
+
config.hook_profile = 'standard';
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
267
423
|
return config;
|
|
268
424
|
}
|
|
269
425
|
|
|
270
|
-
// Loads the two-layer
|
|
426
|
+
// Loads the two-layer nForma config.
|
|
271
427
|
//
|
|
272
|
-
// Layer 1 (global): ~/.claude/
|
|
273
|
-
// Layer 2 (project): <projectDir>/.claude/
|
|
428
|
+
// Layer 1 (global): ~/.claude/nf.json
|
|
429
|
+
// Layer 2 (project): <projectDir>/.claude/nf.json (defaults to process.cwd())
|
|
274
430
|
//
|
|
275
431
|
// Merge is shallow: { ...DEFAULT_CONFIG, ...global, ...project }
|
|
276
432
|
// If both layers are missing/malformed, returns DEFAULT_CONFIG with a warning.
|
|
277
433
|
// All warnings go to stderr — stdout is never touched.
|
|
278
434
|
function loadConfig(projectDir) {
|
|
279
|
-
const globalPath = path.join(os.homedir(), '.claude', '
|
|
280
|
-
const projectPath = path.join(projectDir || process.cwd(), '.claude', '
|
|
435
|
+
const globalPath = path.join(os.homedir(), '.claude', 'nf.json');
|
|
436
|
+
const projectPath = path.join(projectDir || process.cwd(), '.claude', 'nf.json');
|
|
281
437
|
|
|
282
438
|
const globalObj = readConfigFile(globalPath);
|
|
283
439
|
const projectObj = readConfigFile(projectPath);
|
|
284
440
|
|
|
285
441
|
let config;
|
|
286
442
|
if (!globalObj && !projectObj) {
|
|
287
|
-
process.stderr.write('[
|
|
443
|
+
process.stderr.write('[nf] WARNING: No nf.json found at ' + globalPath + ' or ' + projectPath + '; using hardcoded defaults\n');
|
|
288
444
|
config = { ...DEFAULT_CONFIG };
|
|
289
445
|
} else {
|
|
290
446
|
config = { ...DEFAULT_CONFIG, ...(globalObj || {}), ...(projectObj || {}) };
|
|
@@ -294,4 +450,4 @@ function loadConfig(projectDir) {
|
|
|
294
450
|
return config;
|
|
295
451
|
}
|
|
296
452
|
|
|
297
|
-
module.exports = { loadConfig, DEFAULT_CONFIG, SLOT_TOOL_SUFFIX, slotToToolCall };
|
|
453
|
+
module.exports = { loadConfig, validateConfig, DEFAULT_CONFIG, SLOT_TOOL_SUFFIX, slotToToolCall, shouldRunHook, HOOK_PROFILE_MAP };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
// bin/conformance-schema.cjs
|
|
3
3
|
// Single source of truth for conformance event field enumerations.
|
|
4
|
-
// Imported by hooks (
|
|
4
|
+
// Imported by hooks (nf-stop.js, nf-prompt.js, nf-circuit-breaker.js) and validate-traces.cjs.
|
|
5
5
|
// NEVER add external require() calls — hooks have zero runtime dependencies.
|
|
6
6
|
|
|
7
|
-
const VALID_ACTIONS = ['quorum_start', 'quorum_complete', 'quorum_block', 'deliberation_round', 'circuit_break'];
|
|
7
|
+
const VALID_ACTIONS = ['quorum_start', 'quorum_complete', 'quorum_block', 'deliberation_round', 'circuit_break', 'cache_hit', 'budget_warn', 'budget_downgrade', 'stall_detected', 'security_sweep'];
|
|
8
8
|
const VALID_PHASES = ['IDLE', 'COLLECTING_VOTES', 'DELIBERATING', 'DECIDED'];
|
|
9
9
|
const VALID_OUTCOMES = ['APPROVE', 'BLOCK', 'UNAVAILABLE', 'DELIBERATE'];
|
|
10
10
|
const schema_version = '1';
|
|
@@ -1,18 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// hooks/gsd-context-monitor.js
|
|
3
|
-
// PostToolUse hook — context window monitor.
|
|
3
|
+
// PostToolUse hook — context window monitor with budget tracking and smart compact.
|
|
4
4
|
//
|
|
5
5
|
// Reads context_window metrics from the PostToolUse event payload.
|
|
6
6
|
// Injects WARNING or CRITICAL into additionalContext when context usage
|
|
7
|
-
// exceeds configurable thresholds.
|
|
7
|
+
// exceeds configurable thresholds. Also injects budget warnings and
|
|
8
|
+
// smart compact suggestions at clean workflow boundaries.
|
|
9
|
+
// Fails open on all errors.
|
|
8
10
|
//
|
|
9
11
|
// Config: context_monitor.warn_pct (default 70%) and
|
|
10
|
-
// context_monitor.critical_pct (default 90%) in
|
|
12
|
+
// context_monitor.critical_pct (default 90%) in nf.json.
|
|
11
13
|
// Two-layer merge via shared config-loader.
|
|
12
14
|
|
|
13
15
|
'use strict';
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const { loadConfig, shouldRunHook } = require('./config-loader');
|
|
20
|
+
|
|
21
|
+
// Append a conformance event to conformance-events.jsonl (fail-open)
|
|
22
|
+
function appendConformanceEvent(event) {
|
|
23
|
+
try {
|
|
24
|
+
let eventsPath;
|
|
25
|
+
try {
|
|
26
|
+
const planningPaths = require(path.join(__dirname, '..', 'bin', 'planning-paths.cjs'));
|
|
27
|
+
eventsPath = planningPaths.resolveWithFallback(process.cwd(), 'conformance-events');
|
|
28
|
+
} catch {
|
|
29
|
+
eventsPath = path.join(process.cwd(), '.planning', 'telemetry', 'conformance-events.jsonl');
|
|
30
|
+
}
|
|
31
|
+
const dir = path.dirname(eventsPath);
|
|
32
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
fs.appendFileSync(eventsPath, JSON.stringify(event) + '\n', 'utf8');
|
|
34
|
+
} catch {
|
|
35
|
+
// Fail-open
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Detect if the just-completed tool call represents a clean workflow boundary
|
|
40
|
+
function detectCleanBoundary(toolName, toolInput) {
|
|
41
|
+
if (toolName !== 'Bash' || !toolInput) return null;
|
|
42
|
+
const input = typeof toolInput === 'string' ? toolInput : (toolInput.command || '');
|
|
43
|
+
if (input.includes('gsd-tools.cjs phase-complete')) return 'phase_complete';
|
|
44
|
+
if (input.includes('gsd-tools.cjs commit') && input.includes('VERIFICATION')) return 'verification_done';
|
|
45
|
+
if (input.includes('gsd-tools.cjs commit')) return 'commit';
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Format a smart compact suggestion
|
|
50
|
+
function formatCompactSuggestion(usedPct, boundaryType) {
|
|
51
|
+
return `SMART COMPACT SUGGESTION: Context at ${usedPct}% -- clean workflow boundary (${boundaryType}).
|
|
52
|
+
Consider running /compact now.
|
|
53
|
+
|
|
54
|
+
What survives compaction:
|
|
55
|
+
+ STATE.md Current Position (phase, plan, last activity)
|
|
56
|
+
+ Pending task files (.claude/pending-task*.txt)
|
|
57
|
+
+ CLAUDE.md project rules
|
|
58
|
+
|
|
59
|
+
What will be lost:
|
|
60
|
+
- Conversation history and reasoning
|
|
61
|
+
- File contents read during this session
|
|
62
|
+
- Quorum deliberation transcripts
|
|
63
|
+
- Intermediate tool outputs`;
|
|
64
|
+
}
|
|
16
65
|
|
|
17
66
|
let raw = '';
|
|
18
67
|
process.stdin.setEncoding('utf8');
|
|
@@ -21,6 +70,13 @@ process.stdin.on('end', () => {
|
|
|
21
70
|
try {
|
|
22
71
|
const input = JSON.parse(raw);
|
|
23
72
|
|
|
73
|
+
// Profile guard — exit early if this hook is not active for the current profile
|
|
74
|
+
const config = loadConfig(input.cwd || process.cwd());
|
|
75
|
+
const profile = config.hook_profile || 'standard';
|
|
76
|
+
if (!shouldRunHook('gsd-context-monitor', profile)) {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
24
80
|
const ctxWindow = input.context_window;
|
|
25
81
|
if (!ctxWindow || ctxWindow.remaining_percentage == null) {
|
|
26
82
|
process.exit(0); // No context data — fail-open
|
|
@@ -29,30 +85,79 @@ process.stdin.on('end', () => {
|
|
|
29
85
|
const remaining = ctxWindow.remaining_percentage;
|
|
30
86
|
const usedPct = Math.round(100 - remaining);
|
|
31
87
|
|
|
32
|
-
const config = loadConfig(input.cwd || process.cwd());
|
|
33
88
|
const monitorCfg = config.context_monitor || {};
|
|
34
89
|
const warnPct = monitorCfg.warn_pct != null ? monitorCfg.warn_pct : 70;
|
|
35
90
|
const criticalPct = monitorCfg.critical_pct != null ? monitorCfg.critical_pct : 90;
|
|
36
91
|
|
|
37
|
-
|
|
92
|
+
// Context window message
|
|
93
|
+
let contextMessage = null;
|
|
38
94
|
if (usedPct >= criticalPct) {
|
|
39
|
-
|
|
95
|
+
contextMessage =
|
|
40
96
|
`CONTEXT MONITOR CRITICAL: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
|
|
41
97
|
'STOP new work immediately. Save state and inform the user that context is nearly exhausted. ' +
|
|
42
|
-
'
|
|
98
|
+
'Run /nf:pause-work to save execution state.';
|
|
43
99
|
} else if (usedPct >= warnPct) {
|
|
44
|
-
|
|
100
|
+
contextMessage =
|
|
45
101
|
`CONTEXT MONITOR WARNING: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
|
|
46
102
|
'Begin wrapping up current task. Do not start new complex work. ' +
|
|
47
|
-
'
|
|
48
|
-
}
|
|
49
|
-
|
|
103
|
+
'Consider /nf:pause-work to save state.';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Budget tracking
|
|
107
|
+
let budgetMessage = null;
|
|
108
|
+
const budgetTracker = (() => {
|
|
109
|
+
try { return require(path.join(__dirname, '..', 'bin', 'budget-tracker.cjs')); }
|
|
110
|
+
catch { return null; }
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
if (budgetTracker) {
|
|
114
|
+
const status = budgetTracker.computeBudgetStatus(usedPct, config.budget || {}, config.agent_config || {});
|
|
115
|
+
if (status.active && status.shouldDowngrade) {
|
|
116
|
+
const downgradeResult = budgetTracker.triggerProfileDowngrade(input.cwd || process.cwd());
|
|
117
|
+
budgetMessage = budgetTracker.formatBudgetWarning(status, downgradeResult);
|
|
118
|
+
appendConformanceEvent({
|
|
119
|
+
action: 'budget_downgrade',
|
|
120
|
+
ts: new Date().toISOString(),
|
|
121
|
+
budget_used_pct: status.budgetUsedPct,
|
|
122
|
+
estimated_tokens: status.estimatedTokens,
|
|
123
|
+
downgrade: downgradeResult,
|
|
124
|
+
});
|
|
125
|
+
} else if (status.active && status.shouldWarn) {
|
|
126
|
+
budgetMessage = budgetTracker.formatBudgetWarning(status, null);
|
|
127
|
+
appendConformanceEvent({
|
|
128
|
+
action: 'budget_warn',
|
|
129
|
+
ts: new Date().toISOString(),
|
|
130
|
+
budget_used_pct: status.budgetUsedPct,
|
|
131
|
+
estimated_tokens: status.estimatedTokens,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
50
134
|
}
|
|
51
135
|
|
|
136
|
+
// Smart compact suggestion
|
|
137
|
+
let compactMessage = null;
|
|
138
|
+
const smartCfg = config.smart_compact || {};
|
|
139
|
+
if (smartCfg.enabled !== false) {
|
|
140
|
+
const compactThreshold = smartCfg.context_warn_pct || 60;
|
|
141
|
+
if (usedPct >= compactThreshold) {
|
|
142
|
+
const boundary = detectCleanBoundary(input.tool_name, input.tool_input);
|
|
143
|
+
if (boundary) {
|
|
144
|
+
compactMessage = formatCompactSuggestion(usedPct, boundary);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Combine all messages
|
|
150
|
+
const messages = [contextMessage, budgetMessage, compactMessage].filter(Boolean);
|
|
151
|
+
if (messages.length === 0) {
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const combined = messages.join('\n\n');
|
|
156
|
+
|
|
52
157
|
process.stdout.write(JSON.stringify({
|
|
53
158
|
hookSpecificOutput: {
|
|
54
159
|
hookEventName: 'PostToolUse',
|
|
55
|
-
additionalContext:
|
|
160
|
+
additionalContext: combined,
|
|
56
161
|
},
|
|
57
162
|
}));
|
|
58
163
|
process.exit(0);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Check for
|
|
2
|
+
// Check for nForma updates in background, write result to cache
|
|
3
3
|
// Called by SessionStart hook - runs once per session
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
@@ -10,11 +10,11 @@ const { spawn } = require('child_process');
|
|
|
10
10
|
const homeDir = os.homedir();
|
|
11
11
|
const cwd = process.cwd();
|
|
12
12
|
const cacheDir = path.join(homeDir, '.claude', 'cache');
|
|
13
|
-
const cacheFile = path.join(cacheDir, '
|
|
13
|
+
const cacheFile = path.join(cacheDir, 'nf-update-check.json');
|
|
14
14
|
|
|
15
15
|
// VERSION file locations (check project first, then global)
|
|
16
|
-
const projectVersionFile = path.join(cwd, '.claude', '
|
|
17
|
-
const globalVersionFile = path.join(homeDir, '.claude', '
|
|
16
|
+
const projectVersionFile = path.join(cwd, '.claude', 'nf', 'VERSION');
|
|
17
|
+
const globalVersionFile = path.join(homeDir, '.claude', 'nf', 'VERSION');
|
|
18
18
|
|
|
19
19
|
// Ensure cache directory exists
|
|
20
20
|
if (!fs.existsSync(cacheDir)) {
|
|
@@ -42,7 +42,7 @@ const child = spawn(process.execPath, ['-e', `
|
|
|
42
42
|
|
|
43
43
|
let latest = null;
|
|
44
44
|
try {
|
|
45
|
-
latest = execSync('npm view @nforma.ai/
|
|
45
|
+
latest = execSync('npm view @nforma.ai/nforma version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
|
46
46
|
} catch (e) {}
|
|
47
47
|
|
|
48
48
|
const result = {
|