@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.
Files changed (215) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +1024 -0
  3. package/agents/qgsd-codebase-mapper.md +764 -0
  4. package/agents/qgsd-debugger.md +1201 -0
  5. package/agents/qgsd-executor.md +472 -0
  6. package/agents/qgsd-integration-checker.md +443 -0
  7. package/agents/qgsd-phase-researcher.md +502 -0
  8. package/agents/qgsd-plan-checker.md +643 -0
  9. package/agents/qgsd-planner.md +1182 -0
  10. package/agents/qgsd-project-researcher.md +621 -0
  11. package/agents/qgsd-quorum-orchestrator.md +628 -0
  12. package/agents/qgsd-quorum-slot-worker.md +41 -0
  13. package/agents/qgsd-quorum-synthesizer.md +133 -0
  14. package/agents/qgsd-quorum-test-worker.md +37 -0
  15. package/agents/qgsd-quorum-worker.md +161 -0
  16. package/agents/qgsd-research-synthesizer.md +239 -0
  17. package/agents/qgsd-roadmapper.md +660 -0
  18. package/agents/qgsd-verifier.md +628 -0
  19. package/bin/accept-debug-invariant.cjs +165 -0
  20. package/bin/account-manager.cjs +719 -0
  21. package/bin/aggregate-requirements.cjs +466 -0
  22. package/bin/analyze-assumptions.cjs +757 -0
  23. package/bin/analyze-state-space.cjs +921 -0
  24. package/bin/attribute-trace-divergence.cjs +150 -0
  25. package/bin/auth-drivers/gh-cli.cjs +93 -0
  26. package/bin/auth-drivers/index.cjs +46 -0
  27. package/bin/auth-drivers/pool.cjs +67 -0
  28. package/bin/auth-drivers/simple.cjs +95 -0
  29. package/bin/autoClosePtoF.cjs +110 -0
  30. package/bin/blessed-terminal.cjs +350 -0
  31. package/bin/build-phase-index.cjs +472 -0
  32. package/bin/call-quorum-slot.cjs +541 -0
  33. package/bin/ccr-secure-config.cjs +99 -0
  34. package/bin/ccr-secure-start.cjs +83 -0
  35. package/bin/check-bundled-sdks.cjs +177 -0
  36. package/bin/check-coverage-guard.cjs +112 -0
  37. package/bin/check-liveness-fairness.cjs +95 -0
  38. package/bin/check-mcp-health.cjs +123 -0
  39. package/bin/check-provider-health.cjs +395 -0
  40. package/bin/check-results-exit.cjs +24 -0
  41. package/bin/check-spec-sync.cjs +360 -0
  42. package/bin/check-trace-redaction.cjs +271 -0
  43. package/bin/check-trace-schema-drift.cjs +99 -0
  44. package/bin/compareDrift.cjs +21 -0
  45. package/bin/conformance-schema.cjs +12 -0
  46. package/bin/count-scenarios.cjs +420 -0
  47. package/bin/debt-dedup.cjs +144 -0
  48. package/bin/debt-ledger.cjs +61 -0
  49. package/bin/debt-retention.cjs +76 -0
  50. package/bin/debt-state-machine.cjs +80 -0
  51. package/bin/detect-coverage-gaps.cjs +204 -0
  52. package/bin/detect-project-intent.cjs +362 -0
  53. package/bin/export-prism-constants.cjs +164 -0
  54. package/bin/extract-annotations.cjs +633 -0
  55. package/bin/extractFormalExpected.cjs +104 -0
  56. package/bin/fingerprint-drift.cjs +24 -0
  57. package/bin/fingerprint-issue.cjs +46 -0
  58. package/bin/formal-core.cjs +519 -0
  59. package/bin/formal-ref-linker.cjs +141 -0
  60. package/bin/formal-test-sync.cjs +788 -0
  61. package/bin/generate-formal-specs.cjs +588 -0
  62. package/bin/generate-petri-net.cjs +397 -0
  63. package/bin/generate-phase-spec.cjs +249 -0
  64. package/bin/generate-proposed-changes.cjs +194 -0
  65. package/bin/generate-tla-cfg.cjs +122 -0
  66. package/bin/generate-traceability-matrix.cjs +701 -0
  67. package/bin/generate-triage-bundle.cjs +300 -0
  68. package/bin/gh-account-rotate.cjs +34 -0
  69. package/bin/initialize-model-registry.cjs +105 -0
  70. package/bin/install-formal-tools.cjs +382 -0
  71. package/bin/install.js +2424 -0
  72. package/bin/isNumericThreshold.cjs +34 -0
  73. package/bin/issue-classifier.cjs +151 -0
  74. package/bin/levenshtein.cjs +74 -0
  75. package/bin/lint-formal-models.cjs +580 -0
  76. package/bin/load-baseline-requirements.cjs +275 -0
  77. package/bin/manage-agents-core.cjs +815 -0
  78. package/bin/migrate-formal-dir.cjs +172 -0
  79. package/bin/migrate-planning.cjs +206 -0
  80. package/bin/migrate-to-slots.cjs +255 -0
  81. package/bin/nForma.cjs +2726 -0
  82. package/bin/observe-config.cjs +353 -0
  83. package/bin/observe-debt-writer.cjs +140 -0
  84. package/bin/observe-handler-grafana.cjs +128 -0
  85. package/bin/observe-handler-internal.cjs +301 -0
  86. package/bin/observe-handler-logstash.cjs +153 -0
  87. package/bin/observe-handler-prometheus.cjs +185 -0
  88. package/bin/observe-handlers.cjs +436 -0
  89. package/bin/observe-registry.cjs +131 -0
  90. package/bin/observe-render.cjs +168 -0
  91. package/bin/planning-paths.cjs +167 -0
  92. package/bin/polyrepo.cjs +560 -0
  93. package/bin/prism-priority.cjs +153 -0
  94. package/bin/probe-quorum-slots.cjs +167 -0
  95. package/bin/promote-model.cjs +225 -0
  96. package/bin/propose-debug-invariants.cjs +165 -0
  97. package/bin/providers.json +392 -0
  98. package/bin/pty-proxy.py +129 -0
  99. package/bin/qgsd-solve.cjs +2477 -0
  100. package/bin/quorum-consensus-gate.cjs +238 -0
  101. package/bin/quorum-formal-context.cjs +183 -0
  102. package/bin/quorum-slot-dispatch.cjs +934 -0
  103. package/bin/read-policy.cjs +60 -0
  104. package/bin/requirement-map.cjs +63 -0
  105. package/bin/requirements-core.cjs +247 -0
  106. package/bin/resolve-cli.cjs +101 -0
  107. package/bin/review-mcp-logs.cjs +294 -0
  108. package/bin/run-account-manager-tlc.cjs +188 -0
  109. package/bin/run-account-pool-alloy.cjs +158 -0
  110. package/bin/run-alloy.cjs +153 -0
  111. package/bin/run-audit-alloy.cjs +187 -0
  112. package/bin/run-breaker-tlc.cjs +181 -0
  113. package/bin/run-formal-check.cjs +395 -0
  114. package/bin/run-formal-verify.cjs +701 -0
  115. package/bin/run-installer-alloy.cjs +188 -0
  116. package/bin/run-oauth-rotation-prism.cjs +132 -0
  117. package/bin/run-oscillation-tlc.cjs +202 -0
  118. package/bin/run-phase-tlc.cjs +228 -0
  119. package/bin/run-prism.cjs +446 -0
  120. package/bin/run-protocol-tlc.cjs +201 -0
  121. package/bin/run-quorum-composition-alloy.cjs +155 -0
  122. package/bin/run-sensitivity-sweep.cjs +231 -0
  123. package/bin/run-stop-hook-tlc.cjs +188 -0
  124. package/bin/run-tlc.cjs +467 -0
  125. package/bin/run-transcript-alloy.cjs +173 -0
  126. package/bin/run-uppaal.cjs +264 -0
  127. package/bin/secrets.cjs +134 -0
  128. package/bin/sensitivity-report.cjs +219 -0
  129. package/bin/sensitivity-sweep-feedback.cjs +194 -0
  130. package/bin/set-secret.cjs +29 -0
  131. package/bin/setup-telemetry-cron.sh +36 -0
  132. package/bin/sweepPtoF.cjs +63 -0
  133. package/bin/sync-baseline-requirements.cjs +290 -0
  134. package/bin/task-envelope.cjs +360 -0
  135. package/bin/telemetry-collector.cjs +229 -0
  136. package/bin/unified-mcp-server.mjs +735 -0
  137. package/bin/update-agents.cjs +369 -0
  138. package/bin/update-scoreboard.cjs +1134 -0
  139. package/bin/validate-debt-entry.cjs +207 -0
  140. package/bin/validate-invariant.cjs +419 -0
  141. package/bin/validate-memory.cjs +389 -0
  142. package/bin/validate-requirements-haiku.cjs +435 -0
  143. package/bin/validate-traces.cjs +438 -0
  144. package/bin/verify-formal-results.cjs +124 -0
  145. package/bin/verify-quorum-health.cjs +273 -0
  146. package/bin/write-check-result.cjs +106 -0
  147. package/bin/xstate-to-tla.cjs +483 -0
  148. package/bin/xstate-trace-walker.cjs +205 -0
  149. package/commands/qgsd/add-phase.md +43 -0
  150. package/commands/qgsd/add-requirement.md +24 -0
  151. package/commands/qgsd/add-todo.md +47 -0
  152. package/commands/qgsd/audit-milestone.md +37 -0
  153. package/commands/qgsd/check-todos.md +45 -0
  154. package/commands/qgsd/cleanup.md +18 -0
  155. package/commands/qgsd/close-formal-gaps.md +33 -0
  156. package/commands/qgsd/complete-milestone.md +136 -0
  157. package/commands/qgsd/debug.md +166 -0
  158. package/commands/qgsd/discuss-phase.md +83 -0
  159. package/commands/qgsd/execute-phase.md +117 -0
  160. package/commands/qgsd/fix-tests.md +27 -0
  161. package/commands/qgsd/formal-test-sync.md +32 -0
  162. package/commands/qgsd/health.md +22 -0
  163. package/commands/qgsd/help.md +22 -0
  164. package/commands/qgsd/insert-phase.md +32 -0
  165. package/commands/qgsd/join-discord.md +18 -0
  166. package/commands/qgsd/list-phase-assumptions.md +46 -0
  167. package/commands/qgsd/map-codebase.md +71 -0
  168. package/commands/qgsd/map-requirements.md +20 -0
  169. package/commands/qgsd/mcp-restart.md +176 -0
  170. package/commands/qgsd/mcp-set-model.md +134 -0
  171. package/commands/qgsd/mcp-setup.md +1371 -0
  172. package/commands/qgsd/mcp-status.md +274 -0
  173. package/commands/qgsd/mcp-update.md +238 -0
  174. package/commands/qgsd/new-milestone.md +44 -0
  175. package/commands/qgsd/new-project.md +42 -0
  176. package/commands/qgsd/observe.md +260 -0
  177. package/commands/qgsd/pause-work.md +38 -0
  178. package/commands/qgsd/plan-milestone-gaps.md +34 -0
  179. package/commands/qgsd/plan-phase.md +44 -0
  180. package/commands/qgsd/polyrepo.md +50 -0
  181. package/commands/qgsd/progress.md +24 -0
  182. package/commands/qgsd/queue.md +54 -0
  183. package/commands/qgsd/quick.md +133 -0
  184. package/commands/qgsd/quorum-test.md +275 -0
  185. package/commands/qgsd/quorum.md +707 -0
  186. package/commands/qgsd/reapply-patches.md +110 -0
  187. package/commands/qgsd/remove-phase.md +31 -0
  188. package/commands/qgsd/research-phase.md +189 -0
  189. package/commands/qgsd/resume-work.md +40 -0
  190. package/commands/qgsd/set-profile.md +34 -0
  191. package/commands/qgsd/settings.md +39 -0
  192. package/commands/qgsd/solve.md +565 -0
  193. package/commands/qgsd/sync-baselines.md +119 -0
  194. package/commands/qgsd/triage.md +233 -0
  195. package/commands/qgsd/update.md +37 -0
  196. package/commands/qgsd/verify-work.md +38 -0
  197. package/hooks/dist/config-loader.js +297 -0
  198. package/hooks/dist/conformance-schema.cjs +12 -0
  199. package/hooks/dist/gsd-context-monitor.js +64 -0
  200. package/hooks/dist/qgsd-check-update.js +62 -0
  201. package/hooks/dist/qgsd-circuit-breaker.js +682 -0
  202. package/hooks/dist/qgsd-precompact.js +156 -0
  203. package/hooks/dist/qgsd-prompt.js +653 -0
  204. package/hooks/dist/qgsd-session-start.js +122 -0
  205. package/hooks/dist/qgsd-slot-correlator.js +58 -0
  206. package/hooks/dist/qgsd-spec-regen.js +86 -0
  207. package/hooks/dist/qgsd-statusline.js +91 -0
  208. package/hooks/dist/qgsd-stop.js +553 -0
  209. package/hooks/dist/qgsd-token-collector.js +133 -0
  210. package/hooks/dist/unified-mcp-server.mjs +669 -0
  211. package/package.json +95 -0
  212. package/scripts/build-hooks.js +46 -0
  213. package/scripts/postinstall.js +48 -0
  214. package/scripts/secret-audit.sh +45 -0
  215. 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();