@nforma.ai/nforma 0.2.1
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/LICENSE +22 -0
- package/README.md +1024 -0
- package/agents/qgsd-codebase-mapper.md +764 -0
- package/agents/qgsd-debugger.md +1201 -0
- package/agents/qgsd-executor.md +472 -0
- package/agents/qgsd-integration-checker.md +443 -0
- package/agents/qgsd-phase-researcher.md +502 -0
- package/agents/qgsd-plan-checker.md +643 -0
- package/agents/qgsd-planner.md +1182 -0
- package/agents/qgsd-project-researcher.md +621 -0
- package/agents/qgsd-quorum-orchestrator.md +628 -0
- package/agents/qgsd-quorum-slot-worker.md +41 -0
- package/agents/qgsd-quorum-synthesizer.md +133 -0
- package/agents/qgsd-quorum-test-worker.md +37 -0
- package/agents/qgsd-quorum-worker.md +161 -0
- package/agents/qgsd-research-synthesizer.md +239 -0
- package/agents/qgsd-roadmapper.md +660 -0
- package/agents/qgsd-verifier.md +628 -0
- package/bin/accept-debug-invariant.cjs +165 -0
- package/bin/account-manager.cjs +719 -0
- package/bin/aggregate-requirements.cjs +466 -0
- package/bin/analyze-assumptions.cjs +757 -0
- package/bin/analyze-state-space.cjs +921 -0
- package/bin/attribute-trace-divergence.cjs +150 -0
- package/bin/auth-drivers/gh-cli.cjs +93 -0
- package/bin/auth-drivers/index.cjs +46 -0
- package/bin/auth-drivers/pool.cjs +67 -0
- package/bin/auth-drivers/simple.cjs +95 -0
- package/bin/autoClosePtoF.cjs +110 -0
- package/bin/blessed-terminal.cjs +350 -0
- package/bin/build-phase-index.cjs +472 -0
- package/bin/call-quorum-slot.cjs +541 -0
- package/bin/ccr-secure-config.cjs +99 -0
- package/bin/ccr-secure-start.cjs +83 -0
- package/bin/check-bundled-sdks.cjs +177 -0
- package/bin/check-coverage-guard.cjs +112 -0
- package/bin/check-liveness-fairness.cjs +95 -0
- package/bin/check-mcp-health.cjs +123 -0
- package/bin/check-provider-health.cjs +395 -0
- package/bin/check-results-exit.cjs +24 -0
- package/bin/check-spec-sync.cjs +360 -0
- package/bin/check-trace-redaction.cjs +271 -0
- package/bin/check-trace-schema-drift.cjs +99 -0
- package/bin/compareDrift.cjs +21 -0
- package/bin/conformance-schema.cjs +12 -0
- package/bin/count-scenarios.cjs +420 -0
- package/bin/debt-dedup.cjs +144 -0
- package/bin/debt-ledger.cjs +61 -0
- package/bin/debt-retention.cjs +76 -0
- package/bin/debt-state-machine.cjs +80 -0
- package/bin/detect-coverage-gaps.cjs +204 -0
- package/bin/detect-project-intent.cjs +362 -0
- package/bin/export-prism-constants.cjs +164 -0
- package/bin/extract-annotations.cjs +633 -0
- package/bin/extractFormalExpected.cjs +104 -0
- package/bin/fingerprint-drift.cjs +24 -0
- package/bin/fingerprint-issue.cjs +46 -0
- package/bin/formal-core.cjs +519 -0
- package/bin/formal-ref-linker.cjs +141 -0
- package/bin/formal-test-sync.cjs +788 -0
- package/bin/generate-formal-specs.cjs +588 -0
- package/bin/generate-petri-net.cjs +397 -0
- package/bin/generate-phase-spec.cjs +249 -0
- package/bin/generate-proposed-changes.cjs +194 -0
- package/bin/generate-tla-cfg.cjs +122 -0
- package/bin/generate-traceability-matrix.cjs +701 -0
- package/bin/generate-triage-bundle.cjs +300 -0
- package/bin/gh-account-rotate.cjs +34 -0
- package/bin/initialize-model-registry.cjs +105 -0
- package/bin/install-formal-tools.cjs +382 -0
- package/bin/install.js +2424 -0
- package/bin/isNumericThreshold.cjs +34 -0
- package/bin/issue-classifier.cjs +151 -0
- package/bin/levenshtein.cjs +74 -0
- package/bin/lint-formal-models.cjs +580 -0
- package/bin/load-baseline-requirements.cjs +275 -0
- package/bin/manage-agents-core.cjs +815 -0
- package/bin/migrate-formal-dir.cjs +172 -0
- package/bin/migrate-planning.cjs +206 -0
- package/bin/migrate-to-slots.cjs +255 -0
- package/bin/nForma.cjs +2726 -0
- package/bin/observe-config.cjs +353 -0
- package/bin/observe-debt-writer.cjs +140 -0
- package/bin/observe-handler-grafana.cjs +128 -0
- package/bin/observe-handler-internal.cjs +301 -0
- package/bin/observe-handler-logstash.cjs +153 -0
- package/bin/observe-handler-prometheus.cjs +185 -0
- package/bin/observe-handlers.cjs +436 -0
- package/bin/observe-registry.cjs +131 -0
- package/bin/observe-render.cjs +168 -0
- package/bin/planning-paths.cjs +167 -0
- package/bin/polyrepo.cjs +560 -0
- package/bin/prism-priority.cjs +153 -0
- package/bin/probe-quorum-slots.cjs +167 -0
- package/bin/promote-model.cjs +225 -0
- package/bin/propose-debug-invariants.cjs +165 -0
- package/bin/providers.json +392 -0
- package/bin/pty-proxy.py +129 -0
- package/bin/qgsd-solve.cjs +2477 -0
- package/bin/quorum-consensus-gate.cjs +238 -0
- package/bin/quorum-formal-context.cjs +183 -0
- package/bin/quorum-slot-dispatch.cjs +934 -0
- package/bin/read-policy.cjs +60 -0
- package/bin/requirement-map.cjs +63 -0
- package/bin/requirements-core.cjs +247 -0
- package/bin/resolve-cli.cjs +101 -0
- package/bin/review-mcp-logs.cjs +294 -0
- package/bin/run-account-manager-tlc.cjs +188 -0
- package/bin/run-account-pool-alloy.cjs +158 -0
- package/bin/run-alloy.cjs +153 -0
- package/bin/run-audit-alloy.cjs +187 -0
- package/bin/run-breaker-tlc.cjs +181 -0
- package/bin/run-formal-check.cjs +395 -0
- package/bin/run-formal-verify.cjs +701 -0
- package/bin/run-installer-alloy.cjs +188 -0
- package/bin/run-oauth-rotation-prism.cjs +132 -0
- package/bin/run-oscillation-tlc.cjs +202 -0
- package/bin/run-phase-tlc.cjs +228 -0
- package/bin/run-prism.cjs +446 -0
- package/bin/run-protocol-tlc.cjs +201 -0
- package/bin/run-quorum-composition-alloy.cjs +155 -0
- package/bin/run-sensitivity-sweep.cjs +231 -0
- package/bin/run-stop-hook-tlc.cjs +188 -0
- package/bin/run-tlc.cjs +467 -0
- package/bin/run-transcript-alloy.cjs +173 -0
- package/bin/run-uppaal.cjs +264 -0
- package/bin/secrets.cjs +134 -0
- package/bin/sensitivity-report.cjs +219 -0
- package/bin/sensitivity-sweep-feedback.cjs +194 -0
- package/bin/set-secret.cjs +29 -0
- package/bin/setup-telemetry-cron.sh +36 -0
- package/bin/sweepPtoF.cjs +63 -0
- package/bin/sync-baseline-requirements.cjs +290 -0
- package/bin/task-envelope.cjs +360 -0
- package/bin/telemetry-collector.cjs +229 -0
- package/bin/unified-mcp-server.mjs +735 -0
- package/bin/update-agents.cjs +369 -0
- package/bin/update-scoreboard.cjs +1134 -0
- package/bin/validate-debt-entry.cjs +207 -0
- package/bin/validate-invariant.cjs +419 -0
- package/bin/validate-memory.cjs +389 -0
- package/bin/validate-requirements-haiku.cjs +435 -0
- package/bin/validate-traces.cjs +438 -0
- package/bin/verify-formal-results.cjs +124 -0
- package/bin/verify-quorum-health.cjs +273 -0
- package/bin/write-check-result.cjs +106 -0
- package/bin/xstate-to-tla.cjs +483 -0
- package/bin/xstate-trace-walker.cjs +205 -0
- package/commands/qgsd/add-phase.md +43 -0
- package/commands/qgsd/add-requirement.md +24 -0
- package/commands/qgsd/add-todo.md +47 -0
- package/commands/qgsd/audit-milestone.md +37 -0
- package/commands/qgsd/check-todos.md +45 -0
- package/commands/qgsd/cleanup.md +18 -0
- package/commands/qgsd/close-formal-gaps.md +33 -0
- package/commands/qgsd/complete-milestone.md +136 -0
- package/commands/qgsd/debug.md +166 -0
- package/commands/qgsd/discuss-phase.md +83 -0
- package/commands/qgsd/execute-phase.md +117 -0
- package/commands/qgsd/fix-tests.md +27 -0
- package/commands/qgsd/formal-test-sync.md +32 -0
- package/commands/qgsd/health.md +22 -0
- package/commands/qgsd/help.md +22 -0
- package/commands/qgsd/insert-phase.md +32 -0
- package/commands/qgsd/join-discord.md +18 -0
- package/commands/qgsd/list-phase-assumptions.md +46 -0
- package/commands/qgsd/map-codebase.md +71 -0
- package/commands/qgsd/map-requirements.md +20 -0
- package/commands/qgsd/mcp-restart.md +176 -0
- package/commands/qgsd/mcp-set-model.md +134 -0
- package/commands/qgsd/mcp-setup.md +1371 -0
- package/commands/qgsd/mcp-status.md +274 -0
- package/commands/qgsd/mcp-update.md +238 -0
- package/commands/qgsd/new-milestone.md +44 -0
- package/commands/qgsd/new-project.md +42 -0
- package/commands/qgsd/observe.md +260 -0
- package/commands/qgsd/pause-work.md +38 -0
- package/commands/qgsd/plan-milestone-gaps.md +34 -0
- package/commands/qgsd/plan-phase.md +44 -0
- package/commands/qgsd/polyrepo.md +50 -0
- package/commands/qgsd/progress.md +24 -0
- package/commands/qgsd/queue.md +54 -0
- package/commands/qgsd/quick.md +133 -0
- package/commands/qgsd/quorum-test.md +275 -0
- package/commands/qgsd/quorum.md +707 -0
- package/commands/qgsd/reapply-patches.md +110 -0
- package/commands/qgsd/remove-phase.md +31 -0
- package/commands/qgsd/research-phase.md +189 -0
- package/commands/qgsd/resume-work.md +40 -0
- package/commands/qgsd/set-profile.md +34 -0
- package/commands/qgsd/settings.md +39 -0
- package/commands/qgsd/solve.md +565 -0
- package/commands/qgsd/sync-baselines.md +119 -0
- package/commands/qgsd/triage.md +233 -0
- package/commands/qgsd/update.md +37 -0
- package/commands/qgsd/verify-work.md +38 -0
- package/hooks/dist/config-loader.js +297 -0
- package/hooks/dist/conformance-schema.cjs +12 -0
- package/hooks/dist/gsd-context-monitor.js +64 -0
- package/hooks/dist/qgsd-check-update.js +62 -0
- package/hooks/dist/qgsd-circuit-breaker.js +682 -0
- package/hooks/dist/qgsd-precompact.js +156 -0
- package/hooks/dist/qgsd-prompt.js +653 -0
- package/hooks/dist/qgsd-session-start.js +122 -0
- package/hooks/dist/qgsd-slot-correlator.js +58 -0
- package/hooks/dist/qgsd-spec-regen.js +86 -0
- package/hooks/dist/qgsd-statusline.js +91 -0
- package/hooks/dist/qgsd-stop.js +553 -0
- package/hooks/dist/qgsd-token-collector.js +133 -0
- package/hooks/dist/unified-mcp-server.mjs +669 -0
- package/package.json +95 -0
- package/scripts/build-hooks.js +46 -0
- package/scripts/postinstall.js +48 -0
- package/scripts/secret-audit.sh +45 -0
- package/templates/qgsd.json +49 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/config-loader.js
|
|
3
|
+
// Shared two-layer config loader with validation and stderr-only warnings.
|
|
4
|
+
//
|
|
5
|
+
// Exports: loadConfig(projectDir?), DEFAULT_CONFIG
|
|
6
|
+
//
|
|
7
|
+
// Load order: DEFAULT_CONFIG → ~/.claude/qgsd.json (global) → .claude/qgsd.json in projectDir (project)
|
|
8
|
+
// Merge: shallow spread — project values fully replace global values for any overlapping key.
|
|
9
|
+
// Warnings: all written to process.stderr — stdout is never touched (it is the hook decision channel).
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
// Maps the family name of a slot (trailing -N stripped) to the MCP tool suffix to call.
|
|
18
|
+
// Used by both qgsd-prompt.js (step generation) and qgsd-stop.js (evidence detection).
|
|
19
|
+
const SLOT_TOOL_SUFFIX = {
|
|
20
|
+
'codex-cli': 'review',
|
|
21
|
+
'codex': 'review',
|
|
22
|
+
'gemini-cli':'gemini',
|
|
23
|
+
'gemini': 'gemini',
|
|
24
|
+
'opencode': 'opencode',
|
|
25
|
+
'copilot-cli':'ask',
|
|
26
|
+
'copilot': 'ask',
|
|
27
|
+
'claude': 'claude',
|
|
28
|
+
'unified': 'claude',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Returns the recommended tool call name for a slot (e.g. "codex-1" → "mcp__codex-1__review").
|
|
32
|
+
function slotToToolCall(slotName) {
|
|
33
|
+
const family = slotName.replace(/-\d+$/, '');
|
|
34
|
+
const suffix = SLOT_TOOL_SUFFIX[family] || 'claude';
|
|
35
|
+
return 'mcp__' + slotName + '__' + suffix;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_CONFIG = {
|
|
39
|
+
quorum_commands: [
|
|
40
|
+
'plan-phase', 'new-project', 'new-milestone',
|
|
41
|
+
'discuss-phase', 'verify-work', 'research-phase', 'quick',
|
|
42
|
+
],
|
|
43
|
+
fail_mode: 'open',
|
|
44
|
+
required_models: {
|
|
45
|
+
codex: { tool_prefix: 'mcp__codex-cli__', required: true },
|
|
46
|
+
gemini: { tool_prefix: 'mcp__gemini-cli__', required: true },
|
|
47
|
+
opencode: { tool_prefix: 'mcp__opencode__', required: true },
|
|
48
|
+
copilot: { tool_prefix: 'mcp__copilot-cli__', required: true },
|
|
49
|
+
},
|
|
50
|
+
// quorum: pool-based enforcement (supersedes required_models when quorum_active is set).
|
|
51
|
+
// minSize — minimum number of agents that must be called (from the available pool).
|
|
52
|
+
// Default 4 preserves backward compat with the 4-model required_models check.
|
|
53
|
+
// preferSub — sort sub (subscription) agents before api agents in pool and prompt steps.
|
|
54
|
+
quorum: {
|
|
55
|
+
minSize: 4,
|
|
56
|
+
preferSub: false,
|
|
57
|
+
},
|
|
58
|
+
// agent_config: per-slot metadata.
|
|
59
|
+
// auth_type: "sub" (subscription, flat-fee) | "api" (pay-per-token)
|
|
60
|
+
agent_config: {},
|
|
61
|
+
circuit_breaker: {
|
|
62
|
+
oscillation_depth: 3, // how many run-groups of same file set to trigger
|
|
63
|
+
commit_window: 6, // how many commits to look back
|
|
64
|
+
haiku_reviewer: true, // call Claude Haiku to verify before blocking
|
|
65
|
+
haiku_model: 'claude-haiku-4-5-20251001', // model used for review
|
|
66
|
+
},
|
|
67
|
+
model_preferences: {}, // { "<mcp-server-name>": "<model-id>" }
|
|
68
|
+
// context_monitor: PostToolUse hook thresholds for context window warnings.
|
|
69
|
+
// warn_pct — inject WARNING when context used % >= this value (default 70)
|
|
70
|
+
// critical_pct — inject CRITICAL when context used % >= this value (default 90)
|
|
71
|
+
context_monitor: {
|
|
72
|
+
warn_pct: 70,
|
|
73
|
+
critical_pct: 90,
|
|
74
|
+
},
|
|
75
|
+
// quorum_active: array of slot names that participate in quorum.
|
|
76
|
+
// [] = all discovered slots participate (fail-open, backward compatible with pre-Phase-40 installs).
|
|
77
|
+
// A non-empty array is an explicit allowlist.
|
|
78
|
+
// NOTE: loadConfig() uses shallow spread { ...DEFAULT_CONFIG, ...global, ...project } —
|
|
79
|
+
// if project config sets quorum_active, it entirely replaces the global value.
|
|
80
|
+
quorum_active: [],
|
|
81
|
+
// model_tier_planner: model tier for planner agents (gsd-planner, gsd-roadmapper).
|
|
82
|
+
// model_tier_worker: model tier for worker agents (researcher, checker, executor, etc.).
|
|
83
|
+
// Valid values: 'haiku' | 'sonnet' | 'opus'. Flat keys required — nested objects lost in shallow merge.
|
|
84
|
+
model_tier_planner: 'opus',
|
|
85
|
+
model_tier_worker: 'haiku',
|
|
86
|
+
// task_envelope_enabled: master switch for task-envelope.json sidecar writes.
|
|
87
|
+
// Flat key required — nested objects lost in shallow merge.
|
|
88
|
+
task_envelope_enabled: true,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Reads and parses a JSON config file.
|
|
92
|
+
// Returns the parsed object on success.
|
|
93
|
+
// Returns null silently if the file does not exist.
|
|
94
|
+
// Returns null with a stderr warning if the file is malformed.
|
|
95
|
+
function readConfigFile(filePath) {
|
|
96
|
+
if (!fs.existsSync(filePath)) return null;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
process.stderr.write('[qgsd] WARNING: Malformed config at ' + filePath + ': ' + e.message + '\n');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validates config fields in-place.
|
|
106
|
+
// Corrects invalid fields to DEFAULT_CONFIG values and emits a stderr warning for each.
|
|
107
|
+
// Returns the (possibly corrected) config object.
|
|
108
|
+
function validateConfig(config) {
|
|
109
|
+
if (!Array.isArray(config.quorum_commands)) {
|
|
110
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: quorum_commands must be an array; using defaults\n');
|
|
111
|
+
config.quorum_commands = DEFAULT_CONFIG.quorum_commands;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof config.required_models !== 'object' || config.required_models === null) {
|
|
115
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: required_models must be an object; using defaults\n');
|
|
116
|
+
config.required_models = DEFAULT_CONFIG.required_models;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!['open', 'closed'].includes(config.fail_mode)) {
|
|
120
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: fail_mode "' + config.fail_mode + '" invalid; defaulting to "open"\n');
|
|
121
|
+
config.fail_mode = 'open';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Validate circuit_breaker sub-object
|
|
125
|
+
if (typeof config.circuit_breaker !== 'object' || config.circuit_breaker === null) {
|
|
126
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker must be an object; using defaults\n');
|
|
127
|
+
config.circuit_breaker = { ...DEFAULT_CONFIG.circuit_breaker };
|
|
128
|
+
} else {
|
|
129
|
+
// Validate oscillation_depth independently
|
|
130
|
+
if (!Number.isInteger(config.circuit_breaker.oscillation_depth) || config.circuit_breaker.oscillation_depth < 1) {
|
|
131
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.oscillation_depth must be a positive integer; defaulting to 3\n');
|
|
132
|
+
config.circuit_breaker.oscillation_depth = 3;
|
|
133
|
+
}
|
|
134
|
+
// Validate commit_window independently
|
|
135
|
+
if (!Number.isInteger(config.circuit_breaker.commit_window) || config.circuit_breaker.commit_window < 1) {
|
|
136
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.commit_window must be a positive integer; defaulting to 6\n');
|
|
137
|
+
config.circuit_breaker.commit_window = 6;
|
|
138
|
+
}
|
|
139
|
+
// Fill in missing sub-keys with defaults (handles partial circuit_breaker objects)
|
|
140
|
+
if (config.circuit_breaker.oscillation_depth === undefined) {
|
|
141
|
+
config.circuit_breaker.oscillation_depth = DEFAULT_CONFIG.circuit_breaker.oscillation_depth;
|
|
142
|
+
}
|
|
143
|
+
if (config.circuit_breaker.commit_window === undefined) {
|
|
144
|
+
config.circuit_breaker.commit_window = DEFAULT_CONFIG.circuit_breaker.commit_window;
|
|
145
|
+
}
|
|
146
|
+
if (config.circuit_breaker.haiku_reviewer === undefined) {
|
|
147
|
+
config.circuit_breaker.haiku_reviewer = DEFAULT_CONFIG.circuit_breaker.haiku_reviewer;
|
|
148
|
+
}
|
|
149
|
+
if (config.circuit_breaker.haiku_model === undefined) {
|
|
150
|
+
config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
|
|
151
|
+
}
|
|
152
|
+
if (typeof config.circuit_breaker.haiku_reviewer !== 'boolean') {
|
|
153
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.haiku_reviewer must be boolean; defaulting to true\n');
|
|
154
|
+
config.circuit_breaker.haiku_reviewer = true;
|
|
155
|
+
}
|
|
156
|
+
if (typeof config.circuit_breaker.haiku_model !== 'string') {
|
|
157
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: circuit_breaker.haiku_model must be a string; using default\n');
|
|
158
|
+
config.circuit_breaker.haiku_model = DEFAULT_CONFIG.circuit_breaker.haiku_model;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Validate quorum_active
|
|
163
|
+
if (!Array.isArray(config.quorum_active)) {
|
|
164
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: quorum_active must be an array; using []\n');
|
|
165
|
+
config.quorum_active = [];
|
|
166
|
+
} else {
|
|
167
|
+
config.quorum_active = config.quorum_active.filter(
|
|
168
|
+
s => typeof s === 'string' && s.trim().length > 0
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Validate quorum object
|
|
173
|
+
if (typeof config.quorum !== 'object' || config.quorum === null) {
|
|
174
|
+
config.quorum = { ...DEFAULT_CONFIG.quorum };
|
|
175
|
+
} else {
|
|
176
|
+
if (!Number.isInteger(config.quorum.minSize) || config.quorum.minSize < 1) {
|
|
177
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: quorum.minSize must be a positive integer; defaulting to 4\n');
|
|
178
|
+
config.quorum.minSize = DEFAULT_CONFIG.quorum.minSize;
|
|
179
|
+
}
|
|
180
|
+
if (typeof config.quorum.preferSub !== 'boolean') {
|
|
181
|
+
config.quorum.preferSub = DEFAULT_CONFIG.quorum.preferSub;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Validate agent_config
|
|
186
|
+
if (typeof config.agent_config !== 'object' || config.agent_config === null || Array.isArray(config.agent_config)) {
|
|
187
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config must be an object; using {}\n');
|
|
188
|
+
config.agent_config = {};
|
|
189
|
+
} else {
|
|
190
|
+
for (const [slot, meta] of Object.entries(config.agent_config)) {
|
|
191
|
+
if (typeof meta !== 'object' || meta === null) {
|
|
192
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config.' + slot + ' must be an object; removing\n');
|
|
193
|
+
delete config.agent_config[slot];
|
|
194
|
+
} else if (meta.auth_type && !['sub', 'api'].includes(meta.auth_type)) {
|
|
195
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: agent_config.' + slot + '.auth_type must be "sub" or "api"; defaulting to "api"\n');
|
|
196
|
+
meta.auth_type = 'api';
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Validate model_preferences
|
|
202
|
+
if (typeof config.model_preferences !== 'object' || config.model_preferences === null || Array.isArray(config.model_preferences)) {
|
|
203
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: model_preferences must be an object; using {}\n');
|
|
204
|
+
config.model_preferences = {};
|
|
205
|
+
} else {
|
|
206
|
+
// Remove invalid entries (non-string values) with a warning
|
|
207
|
+
for (const [key, val] of Object.entries(config.model_preferences)) {
|
|
208
|
+
if (typeof val !== 'string' || val.trim() === '') {
|
|
209
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: model_preferences.' + key + ' must be a non-empty string; removing\n');
|
|
210
|
+
delete config.model_preferences[key];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Validate context_monitor sub-object
|
|
216
|
+
if (typeof config.context_monitor !== 'object' || config.context_monitor === null) {
|
|
217
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor must be an object; using defaults\n');
|
|
218
|
+
config.context_monitor = { ...DEFAULT_CONFIG.context_monitor };
|
|
219
|
+
} else {
|
|
220
|
+
if (!Number.isInteger(config.context_monitor.warn_pct) ||
|
|
221
|
+
config.context_monitor.warn_pct < 1 || config.context_monitor.warn_pct > 99) {
|
|
222
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.warn_pct must be an integer 1-99; defaulting to 70\n');
|
|
223
|
+
config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
|
|
224
|
+
}
|
|
225
|
+
if (!Number.isInteger(config.context_monitor.critical_pct) ||
|
|
226
|
+
config.context_monitor.critical_pct < 1 || config.context_monitor.critical_pct > 100) {
|
|
227
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.critical_pct must be an integer 1-100; defaulting to 90\n');
|
|
228
|
+
config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
|
|
229
|
+
}
|
|
230
|
+
if (config.context_monitor.warn_pct >= config.context_monitor.critical_pct) {
|
|
231
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: context_monitor.warn_pct must be less than critical_pct; resetting to defaults\n');
|
|
232
|
+
config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
|
|
233
|
+
config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
|
|
234
|
+
}
|
|
235
|
+
// Fill missing sub-keys with defaults
|
|
236
|
+
if (config.context_monitor.warn_pct === undefined) {
|
|
237
|
+
config.context_monitor.warn_pct = DEFAULT_CONFIG.context_monitor.warn_pct;
|
|
238
|
+
}
|
|
239
|
+
if (config.context_monitor.critical_pct === undefined) {
|
|
240
|
+
config.context_monitor.critical_pct = DEFAULT_CONFIG.context_monitor.critical_pct;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Validate model_tier_planner and model_tier_worker
|
|
245
|
+
const VALID_TIERS = ['haiku', 'sonnet', 'opus'];
|
|
246
|
+
if (config.model_tier_planner !== undefined) {
|
|
247
|
+
if (typeof config.model_tier_planner !== 'string' || !VALID_TIERS.includes(config.model_tier_planner)) {
|
|
248
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: model_tier_planner must be "haiku", "sonnet", or "opus"; removing\n');
|
|
249
|
+
delete config.model_tier_planner;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (config.model_tier_worker !== undefined) {
|
|
253
|
+
if (typeof config.model_tier_worker !== 'string' || !VALID_TIERS.includes(config.model_tier_worker)) {
|
|
254
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: model_tier_worker must be "haiku", "sonnet", or "opus"; removing\n');
|
|
255
|
+
delete config.model_tier_worker;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Validate task_envelope_enabled
|
|
260
|
+
if (config.task_envelope_enabled !== undefined) {
|
|
261
|
+
if (typeof config.task_envelope_enabled !== 'boolean') {
|
|
262
|
+
process.stderr.write('[qgsd] WARNING: qgsd.json: task_envelope_enabled must be a boolean; using default true\n');
|
|
263
|
+
config.task_envelope_enabled = true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return config;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Loads the two-layer QGSD config.
|
|
271
|
+
//
|
|
272
|
+
// Layer 1 (global): ~/.claude/qgsd.json
|
|
273
|
+
// Layer 2 (project): <projectDir>/.claude/qgsd.json (defaults to process.cwd())
|
|
274
|
+
//
|
|
275
|
+
// Merge is shallow: { ...DEFAULT_CONFIG, ...global, ...project }
|
|
276
|
+
// If both layers are missing/malformed, returns DEFAULT_CONFIG with a warning.
|
|
277
|
+
// All warnings go to stderr — stdout is never touched.
|
|
278
|
+
function loadConfig(projectDir) {
|
|
279
|
+
const globalPath = path.join(os.homedir(), '.claude', 'qgsd.json');
|
|
280
|
+
const projectPath = path.join(projectDir || process.cwd(), '.claude', 'qgsd.json');
|
|
281
|
+
|
|
282
|
+
const globalObj = readConfigFile(globalPath);
|
|
283
|
+
const projectObj = readConfigFile(projectPath);
|
|
284
|
+
|
|
285
|
+
let config;
|
|
286
|
+
if (!globalObj && !projectObj) {
|
|
287
|
+
process.stderr.write('[qgsd] WARNING: No qgsd.json found at ' + globalPath + ' or ' + projectPath + '; using hardcoded defaults\n');
|
|
288
|
+
config = { ...DEFAULT_CONFIG };
|
|
289
|
+
} else {
|
|
290
|
+
config = { ...DEFAULT_CONFIG, ...(globalObj || {}), ...(projectObj || {}) };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
validateConfig(config);
|
|
294
|
+
return config;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = { loadConfig, DEFAULT_CONFIG, SLOT_TOOL_SUFFIX, slotToToolCall };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// bin/conformance-schema.cjs
|
|
3
|
+
// Single source of truth for conformance event field enumerations.
|
|
4
|
+
// Imported by hooks (qgsd-stop.js, qgsd-prompt.js, qgsd-circuit-breaker.js) and validate-traces.cjs.
|
|
5
|
+
// NEVER add external require() calls — hooks have zero runtime dependencies.
|
|
6
|
+
|
|
7
|
+
const VALID_ACTIONS = ['quorum_start', 'quorum_complete', 'quorum_block', 'deliberation_round', 'circuit_break'];
|
|
8
|
+
const VALID_PHASES = ['IDLE', 'COLLECTING_VOTES', 'DELIBERATING', 'DECIDED'];
|
|
9
|
+
const VALID_OUTCOMES = ['APPROVE', 'BLOCK', 'UNAVAILABLE', 'DELIBERATE'];
|
|
10
|
+
const schema_version = '1';
|
|
11
|
+
|
|
12
|
+
module.exports = { VALID_ACTIONS, VALID_PHASES, VALID_OUTCOMES, schema_version };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/gsd-context-monitor.js
|
|
3
|
+
// PostToolUse hook — context window monitor.
|
|
4
|
+
//
|
|
5
|
+
// Reads context_window metrics from the PostToolUse event payload.
|
|
6
|
+
// Injects WARNING or CRITICAL into additionalContext when context usage
|
|
7
|
+
// exceeds configurable thresholds. Fails open on all errors.
|
|
8
|
+
//
|
|
9
|
+
// Config: context_monitor.warn_pct (default 70%) and
|
|
10
|
+
// context_monitor.critical_pct (default 90%) in qgsd.json.
|
|
11
|
+
// Two-layer merge via shared config-loader.
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { loadConfig } = require('./config-loader');
|
|
16
|
+
|
|
17
|
+
let raw = '';
|
|
18
|
+
process.stdin.setEncoding('utf8');
|
|
19
|
+
process.stdin.on('data', chunk => { raw += chunk; });
|
|
20
|
+
process.stdin.on('end', () => {
|
|
21
|
+
try {
|
|
22
|
+
const input = JSON.parse(raw);
|
|
23
|
+
|
|
24
|
+
const ctxWindow = input.context_window;
|
|
25
|
+
if (!ctxWindow || ctxWindow.remaining_percentage == null) {
|
|
26
|
+
process.exit(0); // No context data — fail-open
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const remaining = ctxWindow.remaining_percentage;
|
|
30
|
+
const usedPct = Math.round(100 - remaining);
|
|
31
|
+
|
|
32
|
+
const config = loadConfig(input.cwd || process.cwd());
|
|
33
|
+
const monitorCfg = config.context_monitor || {};
|
|
34
|
+
const warnPct = monitorCfg.warn_pct != null ? monitorCfg.warn_pct : 70;
|
|
35
|
+
const criticalPct = monitorCfg.critical_pct != null ? monitorCfg.critical_pct : 90;
|
|
36
|
+
|
|
37
|
+
let message;
|
|
38
|
+
if (usedPct >= criticalPct) {
|
|
39
|
+
message =
|
|
40
|
+
`CONTEXT MONITOR CRITICAL: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
|
|
41
|
+
'STOP new work immediately. Save state and inform the user that context is nearly exhausted. ' +
|
|
42
|
+
'If using QGSD, run /qgsd:pause-work to save execution state.';
|
|
43
|
+
} else if (usedPct >= warnPct) {
|
|
44
|
+
message =
|
|
45
|
+
`CONTEXT MONITOR WARNING: Context window ${usedPct}% used (${Math.round(remaining)}% remaining). ` +
|
|
46
|
+
'Begin wrapping up current task. Do not start new complex work. ' +
|
|
47
|
+
'If using QGSD, consider /qgsd:pause-work to save state.';
|
|
48
|
+
} else {
|
|
49
|
+
process.exit(0); // Below warning threshold — no injection needed
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
process.stdout.write(JSON.stringify({
|
|
53
|
+
hookSpecificOutput: {
|
|
54
|
+
hookEventName: 'PostToolUse',
|
|
55
|
+
additionalContext: message,
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// Fail-open: never crash the user's session on any unexpected error
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Check for QGSD updates in background, write result to cache
|
|
3
|
+
// Called by SessionStart hook - runs once per session
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const homeDir = os.homedir();
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const cacheDir = path.join(homeDir, '.claude', 'cache');
|
|
13
|
+
const cacheFile = path.join(cacheDir, 'qgsd-update-check.json');
|
|
14
|
+
|
|
15
|
+
// VERSION file locations (check project first, then global)
|
|
16
|
+
const projectVersionFile = path.join(cwd, '.claude', 'qgsd', 'VERSION');
|
|
17
|
+
const globalVersionFile = path.join(homeDir, '.claude', 'qgsd', 'VERSION');
|
|
18
|
+
|
|
19
|
+
// Ensure cache directory exists
|
|
20
|
+
if (!fs.existsSync(cacheDir)) {
|
|
21
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Run check in background (spawn background process, windowsHide prevents console flash)
|
|
25
|
+
const child = spawn(process.execPath, ['-e', `
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const { execSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
const cacheFile = ${JSON.stringify(cacheFile)};
|
|
30
|
+
const projectVersionFile = ${JSON.stringify(projectVersionFile)};
|
|
31
|
+
const globalVersionFile = ${JSON.stringify(globalVersionFile)};
|
|
32
|
+
|
|
33
|
+
// Check project directory first (local install), then global
|
|
34
|
+
let installed = '0.0.0';
|
|
35
|
+
try {
|
|
36
|
+
if (fs.existsSync(projectVersionFile)) {
|
|
37
|
+
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
|
|
38
|
+
} else if (fs.existsSync(globalVersionFile)) {
|
|
39
|
+
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
|
|
43
|
+
let latest = null;
|
|
44
|
+
try {
|
|
45
|
+
latest = execSync('npm view @nforma.ai/qgsd version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
|
|
46
|
+
} catch (e) {}
|
|
47
|
+
|
|
48
|
+
const result = {
|
|
49
|
+
update_available: latest && installed !== latest,
|
|
50
|
+
installed,
|
|
51
|
+
latest: latest || 'unknown',
|
|
52
|
+
checked: Math.floor(Date.now() / 1000)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
fs.writeFileSync(cacheFile, JSON.stringify(result));
|
|
56
|
+
`], {
|
|
57
|
+
stdio: 'ignore',
|
|
58
|
+
windowsHide: true,
|
|
59
|
+
detached: true // Required on Windows for proper process detachment
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
child.unref();
|