@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,83 @@
1
+ #!/usr/bin/env node
2
+ // bin/ccr-secure-start.cjs
3
+ // Wrapper script for claude-code-router that:
4
+ // 1. Populates ~/.claude-code-router/config.json from keytar before starting CCR
5
+ // 2. Spawns CCR with the provided args
6
+ // 3. Wipes all provider api_key fields from config.json on exit or signal
7
+ //
8
+ // Usage: node bin/ccr-secure-start.cjs <ccr-binary> [args...]
9
+ // Example: node bin/ccr-secure-start.cjs ccr start
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const { execFileSync, spawn } = require('child_process');
17
+
18
+ const CONFIG_PATH = path.join(os.homedir(), '.claude-code-router', 'config.json');
19
+ const SECURE_CONFIG = path.join(__dirname, 'ccr-secure-config.cjs');
20
+
21
+ // Synchronously wipe all api_key fields in config.json to empty strings.
22
+ // Called on exit and on signal to ensure keys are not left on disk.
23
+ function wipeKeys() {
24
+ try {
25
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
26
+ const config = JSON.parse(raw);
27
+ if (Array.isArray(config.providers)) {
28
+ for (const provider of config.providers) {
29
+ provider.api_key = '';
30
+ }
31
+ }
32
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
33
+ } catch (_) {
34
+ // Fail silently — config may not exist or may already be wiped
35
+ }
36
+ }
37
+
38
+ async function main() {
39
+ const ccrBin = process.argv[2];
40
+ const ccrArgs = process.argv.slice(3);
41
+
42
+ if (!ccrBin) {
43
+ process.stderr.write('Usage: node ccr-secure-start.cjs <ccr-binary> [args...]\n');
44
+ process.stderr.write('Example: node ccr-secure-start.cjs ccr start\n');
45
+ process.exit(1);
46
+ }
47
+
48
+ // Step 1: Populate config.json from keytar
49
+ try {
50
+ execFileSync(process.execPath, [SECURE_CONFIG], { stdio: 'inherit' });
51
+ } catch (e) {
52
+ process.stderr.write('[ccr-secure-start] Failed to populate CCR config: ' + e.message + '\n');
53
+ process.exit(1);
54
+ }
55
+
56
+ // Step 2: Spawn CCR
57
+ const child = spawn(ccrBin, ccrArgs, { stdio: 'inherit' });
58
+
59
+ // Step 3: Wipe keys on CCR exit
60
+ child.on('exit', (code) => {
61
+ wipeKeys();
62
+ process.exit(code ?? 0);
63
+ });
64
+
65
+ // Wipe keys on SIGTERM (e.g. systemd stop, kill)
66
+ process.on('SIGTERM', () => {
67
+ child.kill('SIGTERM');
68
+ wipeKeys();
69
+ process.exit(0);
70
+ });
71
+
72
+ // Wipe keys on SIGINT (Ctrl-C)
73
+ process.on('SIGINT', () => {
74
+ child.kill('SIGINT');
75
+ wipeKeys();
76
+ process.exit(0);
77
+ });
78
+ }
79
+
80
+ main().catch((e) => {
81
+ process.stderr.write('[ccr-secure-start] Unexpected error: ' + e.message + '\n');
82
+ process.exit(1);
83
+ });
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Forbidden SDK list — LLM SDKs that QGSD must not bundle (ARCH-10)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const FORBIDDEN_SDKS = [
12
+ '@anthropic-ai/sdk',
13
+ 'anthropic',
14
+ 'openai',
15
+ '@google/generative-ai',
16
+ 'google-generativeai',
17
+ 'cohere-ai',
18
+ '@azure/openai',
19
+ ];
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Import pattern builder
23
+ // ---------------------------------------------------------------------------
24
+
25
+ function buildImportPatterns() {
26
+ return FORBIDDEN_SDKS.map(sdk => {
27
+ // Escape special regex chars in SDK name (@ and /)
28
+ const escaped = sdk.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ return {
30
+ sdk,
31
+ patterns: [
32
+ // CommonJS require
33
+ new RegExp(`require\\s*\\(\\s*['"]${escaped}['"]\\s*\\)`),
34
+ // require.resolve
35
+ new RegExp(`require\\.resolve\\s*\\(\\s*['"]${escaped}['"]\\s*\\)`),
36
+ // ESM import
37
+ new RegExp(`import\\s+.*\\s+from\\s+['"]${escaped}['"]`),
38
+ ],
39
+ };
40
+ });
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Scope filtering
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function isInScope(filePath) {
48
+ const normalized = filePath.replace(/\\/g, '/');
49
+
50
+ // Must be under bin/ or hooks/
51
+ const inScope = normalized.startsWith('bin/') || normalized.startsWith('hooks/') ||
52
+ normalized.includes('/bin/') || normalized.includes('/hooks/');
53
+ if (!inScope) return false;
54
+
55
+ // Must have .js, .cjs, or .mjs extension
56
+ if (!/\.(js|cjs|mjs)$/.test(normalized)) return false;
57
+
58
+ // Exclude test files
59
+ if (/\.test\.(js|cjs|mjs)$/.test(normalized)) return false;
60
+
61
+ // Exclude node_modules, docs, examples, .planning/formal
62
+ if (/node_modules|\/docs\/|\/examples\/|\.planning\/formal/.test(normalized)) return false;
63
+
64
+ return true;
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Line-level scanning
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function scanFile(filePath) {
72
+ let content;
73
+ try {
74
+ content = fs.readFileSync(filePath, 'utf8');
75
+ } catch (_) {
76
+ // Fail-open on read errors
77
+ return [];
78
+ }
79
+
80
+ const lines = content.split('\n');
81
+ const importPatterns = buildImportPatterns();
82
+ const violations = [];
83
+
84
+ for (let i = 0; i < lines.length; i++) {
85
+ const line = lines[i];
86
+ const trimmed = line.trimStart();
87
+
88
+ // Skip single-line comments
89
+ if (trimmed.startsWith('//')) continue;
90
+
91
+ for (const { sdk, patterns } of importPatterns) {
92
+ for (const pattern of patterns) {
93
+ if (pattern.test(line)) {
94
+ violations.push({
95
+ file: filePath,
96
+ line: i + 1,
97
+ content: line.trim(),
98
+ sdk,
99
+ violation: 'SDK_IMPORT',
100
+ });
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ return violations;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // File walker
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function getAllSourceFiles(dir) {
114
+ const results = [];
115
+ const skipDirs = new Set(['node_modules', '.git', 'dist', '.formal']);
116
+
117
+ function walk(currentDir) {
118
+ let entries;
119
+ try {
120
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
121
+ } catch (_) {
122
+ return;
123
+ }
124
+
125
+ for (const entry of entries) {
126
+ if (entry.isDirectory()) {
127
+ if (!skipDirs.has(entry.name)) {
128
+ walk(path.join(currentDir, entry.name));
129
+ }
130
+ } else if (entry.isFile() && /\.(js|cjs|mjs)$/.test(entry.name)) {
131
+ results.push(path.join(currentDir, entry.name));
132
+ }
133
+ }
134
+ }
135
+
136
+ walk(dir);
137
+ return results;
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Main
142
+ // ---------------------------------------------------------------------------
143
+
144
+ function main() {
145
+ console.log('[LINT] Architecture: SDK Bundling Check');
146
+
147
+ const allFiles = getAllSourceFiles('.');
148
+ const scopedFiles = allFiles
149
+ .map(f => f.startsWith('./') ? f.slice(2) : f)
150
+ .filter(isInScope);
151
+
152
+ console.log(`Scanned ${scopedFiles.length} files`);
153
+
154
+ const allViolations = [];
155
+ for (const file of scopedFiles) {
156
+ const violations = scanFile(file);
157
+ allViolations.push(...violations);
158
+ }
159
+
160
+ console.log(`Found ${allViolations.length} violations`);
161
+
162
+ for (const v of allViolations) {
163
+ console.log(` ERROR: ${v.file}:${v.line}: ${v.content}`);
164
+ }
165
+
166
+ process.exit(allViolations.length > 0 ? 1 : 0);
167
+ }
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Exports + CLI entry
171
+ // ---------------------------------------------------------------------------
172
+
173
+ module.exports = { FORBIDDEN_SDKS, buildImportPatterns, isInScope, scanFile, getAllSourceFiles };
174
+
175
+ if (require.main === module) {
176
+ main();
177
+ }
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/check-coverage-guard.cjs
4
+ // CI coverage guard — compares current traceability matrix coverage against
5
+ // a saved baseline and warns when the drop exceeds a configurable threshold.
6
+ //
7
+ // Usage:
8
+ // node bin/check-coverage-guard.cjs # compare current vs baseline
9
+ // node bin/check-coverage-guard.cjs --save-baseline # save current matrix as baseline
10
+ // node bin/check-coverage-guard.cjs --threshold 10 # override threshold (default: 15)
11
+ // node bin/check-coverage-guard.cjs --quiet # suppress output (exit code only)
12
+ //
13
+ // Requirements: TRACE-05
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const TAG = '[check-coverage-guard]';
19
+ let ROOT = process.cwd();
20
+
21
+ // Parse --project-root (overrides CWD-based ROOT for cross-repo usage)
22
+ for (const arg of process.argv.slice(2)) {
23
+ if (arg.startsWith('--project-root=')) {
24
+ ROOT = path.resolve(arg.slice('--project-root='.length));
25
+ }
26
+ }
27
+
28
+ const MATRIX_PATH = process.env.COVERAGE_GUARD_MATRIX_PATH || path.join(ROOT, '.planning', 'formal', 'traceability-matrix.json');
29
+ const BASELINE_PATH = process.env.COVERAGE_GUARD_BASELINE_PATH || path.join(ROOT, '.planning', 'formal', 'traceability-matrix.baseline.json');
30
+
31
+ // ── CLI flags ───────────────────────────────────────────────────────────────
32
+
33
+ const args = process.argv.slice(2);
34
+ const saveBaseline = args.includes('--save-baseline');
35
+ const quietMode = args.includes('--quiet');
36
+ const thresholdIdx = args.indexOf('--threshold');
37
+ const threshold = thresholdIdx !== -1 ? parseFloat(args[thresholdIdx + 1]) : 15;
38
+
39
+ // ── Helpers ─────────────────────────────────────────────────────────────────
40
+
41
+ function log(msg) {
42
+ if (!quietMode) {
43
+ process.stdout.write(TAG + ' ' + msg + '\n');
44
+ }
45
+ }
46
+
47
+ function loadJSON(filePath) {
48
+ try {
49
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
50
+ } catch (err) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ // ── Save Baseline Mode ──────────────────────────────────────────────────────
56
+
57
+ if (saveBaseline) {
58
+ const current = loadJSON(MATRIX_PATH);
59
+ if (!current) {
60
+ process.stderr.write(TAG + ' ERROR: Cannot read ' + MATRIX_PATH + '\n');
61
+ process.exit(1);
62
+ }
63
+ fs.writeFileSync(BASELINE_PATH, JSON.stringify(current, null, 2) + '\n', 'utf8');
64
+ const cs = current.coverage_summary || {};
65
+ log('Baseline saved: ' + (cs.coverage_percentage || 0) + '% coverage (' + (cs.covered_count || 0) + '/' + (cs.total_requirements || 0) + ' requirements)');
66
+ process.exit(0);
67
+ }
68
+
69
+ // ── Comparison Mode ─────────────────────────────────────────────────────────
70
+
71
+ const current = loadJSON(MATRIX_PATH);
72
+ if (!current) {
73
+ process.stderr.write(TAG + ' ERROR: Cannot read current matrix at ' + MATRIX_PATH + '\n');
74
+ process.exit(1);
75
+ }
76
+
77
+ let baseline = loadJSON(BASELINE_PATH);
78
+
79
+ if (!baseline) {
80
+ // Auto-create baseline from current matrix
81
+ fs.writeFileSync(BASELINE_PATH, JSON.stringify(current, null, 2) + '\n', 'utf8');
82
+ const cs = current.coverage_summary || {};
83
+ log('No baseline found — creating from current matrix (' + (cs.coverage_percentage || 0) + '%)');
84
+ log('PASS: No regression (baseline just created)');
85
+ process.exit(0);
86
+ }
87
+
88
+ // Extract coverage percentages
89
+ const currentCs = current.coverage_summary || {};
90
+ const baselineCs = baseline.coverage_summary || {};
91
+ const currentPct = currentCs.coverage_percentage || 0;
92
+ const baselinePct = baselineCs.coverage_percentage || 0;
93
+ const drop = Math.round((baselinePct - currentPct) * 10) / 10;
94
+
95
+ if (drop > threshold) {
96
+ // Coverage regression exceeds threshold
97
+ log('WARN: Coverage regression detected');
98
+ log(' Baseline: ' + baselinePct + '% (' + (baselineCs.covered_count || 0) + '/' + (baselineCs.total_requirements || 0) + ')');
99
+ log(' Current: ' + currentPct + '% (' + (currentCs.covered_count || 0) + '/' + (currentCs.total_requirements || 0) + ')');
100
+ log(' Drop: ' + drop + ' percentage points (threshold: ' + threshold + 'pp)');
101
+ log(' To update baseline: node bin/check-coverage-guard.cjs --save-baseline');
102
+ process.exit(1);
103
+ } else if (drop > 0) {
104
+ // Minor drop within threshold
105
+ log('PASS: Minor coverage change (' + drop + 'pp drop, within ' + threshold + 'pp threshold)');
106
+ log(' Baseline: ' + baselinePct + '% → Current: ' + currentPct + '%');
107
+ process.exit(0);
108
+ } else {
109
+ // No regression or improved
110
+ log('PASS: No regression (baseline: ' + baselinePct + '% → current: ' + currentPct + '%)');
111
+ process.exit(0);
112
+ }
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/check-liveness-fairness.cjs
4
+ // CI step: scan all TLA+ MC*.cfg files for liveness properties lacking fairness declarations.
5
+ // Emits result='pass' if all fairness declarations found; result='inconclusive' if any missing.
6
+ // Always exits 0 (inconclusive is not a build failure).
7
+ // Requirements: LIVE-01, LIVE-02
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { detectLivenessProperties } = require('./run-tlc.cjs');
12
+ const { writeCheckResult } = require('./write-check-result.cjs');
13
+ const { getRequirementIds } = require('./requirement-map.cjs');
14
+
15
+ const TAG = '[check-liveness-fairness]';
16
+
17
+ // Allow env var overrides for testing (injected by test scaffold)
18
+ const FORMAL_TLA_DIR = process.env.FORMAL_TLA_DIR || path.join(__dirname, '..', '.planning', 'formal', 'tla');
19
+ const FORMAL_SPEC_DIR = process.env.FORMAL_SPEC_DIR || path.join(__dirname, '..', '.planning', 'formal', 'spec');
20
+
21
+ const startMs = Date.now();
22
+
23
+ // 1. Discover all MC*.cfg files dynamically
24
+ let cfgFiles;
25
+ try {
26
+ cfgFiles = fs.readdirSync(FORMAL_TLA_DIR)
27
+ .filter(f => /^MC.*\.cfg$/.test(f))
28
+ .map(f => ({ name: f.replace(/\.cfg$/, ''), path: path.join(FORMAL_TLA_DIR, f) }));
29
+ } catch (e) {
30
+ process.stderr.write(TAG + ' Warning: cannot read .planning/formal/tla dir: ' + e.message + '\n');
31
+ cfgFiles = [];
32
+ }
33
+
34
+ // 2. For each config, detect liveness properties without fairness declarations
35
+ const allMissing = []; // { config, properties: string[] }
36
+ let configsChecked = 0;
37
+
38
+ for (const { name, path: cfgPath } of cfgFiles) {
39
+ const missing = detectLivenessProperties(name, cfgPath, FORMAL_SPEC_DIR);
40
+ configsChecked++;
41
+ if (missing.length > 0) {
42
+ allMissing.push({ config: name, properties: missing });
43
+ }
44
+ }
45
+
46
+ // 3. Aggregate results
47
+ const runtimeMs = Date.now() - startMs;
48
+ const hasMissing = allMissing.length > 0;
49
+ const result = hasMissing ? 'inconclusive' : 'pass';
50
+
51
+ const missingList = allMissing
52
+ .map(({ config, properties }) => config + ': ' + properties.join(', '))
53
+ .join('; ');
54
+
55
+ const summaryText = hasMissing
56
+ ? 'inconclusive: fairness declarations missing — ' + missingList
57
+ : 'pass: all liveness properties have fairness declarations (' + configsChecked + ' configs checked)';
58
+
59
+ // 4. Write v2.1 check result
60
+ try {
61
+ writeCheckResult({
62
+ tool: 'check-liveness-fairness',
63
+ formalism: 'tla',
64
+ result,
65
+ check_id: 'ci:liveness-fairness-lint',
66
+ surface: 'ci',
67
+ property: 'Liveness fairness declarations — all TLA+ liveness properties documented with WF/SF rationale',
68
+ runtime_ms: runtimeMs,
69
+ summary: summaryText,
70
+ triage_tags: hasMissing ? ['needs-fairness'] : [],
71
+ requirement_ids: getRequirementIds('ci:liveness-fairness-lint'),
72
+ metadata: {
73
+ configs_checked: configsChecked,
74
+ configs_missing: allMissing.length,
75
+ missing_detail: allMissing,
76
+ },
77
+ });
78
+ } catch (e) {
79
+ process.stderr.write(TAG + ' Warning: failed to write check result: ' + e.message + '\n');
80
+ }
81
+
82
+ // 5. Print summary
83
+ process.stdout.write(TAG + ' Result: ' + result + '\n');
84
+ if (hasMissing) {
85
+ process.stdout.write(TAG + ' Missing fairness declarations:\n');
86
+ for (const { config, properties } of allMissing) {
87
+ process.stdout.write(' ' + config + ': ' + properties.join(', ') + '\n');
88
+ }
89
+ process.stdout.write(TAG + ' Add ## <PropertyName> sections to the matching .planning/formal/spec/<spec>/invariants.md\n');
90
+ } else {
91
+ process.stdout.write(TAG + ' All ' + configsChecked + ' configs have fairness declarations.\n');
92
+ }
93
+
94
+ // Always exit 0 — inconclusive is not a build failure
95
+ process.exit(0);
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * check-mcp-health.cjs
6
+ *
7
+ * Pre-flight health check for all claude-mcp-server instances.
8
+ * Reads ~/.claude.json to find configured MCP servers, then calls
9
+ * health_check on each one sequentially. Exits non-zero if any
10
+ * configured server is unhealthy.
11
+ *
12
+ * Usage:
13
+ * node bin/check-mcp-health.cjs [--timeout-ms N] [--json]
14
+ *
15
+ * Designed to be called at the start of /qgsd:quorum to skip
16
+ * unresponsive servers before making full inference calls.
17
+ */
18
+
19
+ const { execFileSync } = require('child_process');
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const os = require('os');
23
+
24
+ const args = process.argv.slice(2);
25
+ const getArg = (f) => { const i = args.indexOf(f); return i !== -1 && args[i+1] ? args[i+1] : null; };
26
+ const hasFlag = (f) => args.includes(f);
27
+ const TIMEOUT_MS = parseInt(getArg('--timeout-ms') ?? '12000', 10);
28
+ const JSON_OUT = hasFlag('--json');
29
+
30
+ // ─── Load MCP server list from ~/.claude.json ─────────────────────────────────
31
+ let mcpServers = {};
32
+ try {
33
+ const raw = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
34
+ mcpServers = raw.mcpServers ?? {};
35
+ } catch (e) {
36
+ console.error('Could not read ~/.claude.json:', e.message);
37
+ process.exit(1);
38
+ }
39
+
40
+ // Filter to claude-mcp-server instances (those using our dist/index.js)
41
+ const targets = Object.entries(mcpServers)
42
+ .filter(([, v]) => v.args?.some(a => a.includes('claude-mcp-server')))
43
+ .map(([name, cfg]) => ({ name, env: cfg.env ?? {} }));
44
+
45
+ if (targets.length === 0) {
46
+ console.log('No claude-mcp-server instances found in ~/.claude.json');
47
+ process.exit(0);
48
+ }
49
+
50
+ // ─── Run health check via MCP JSON-RPC over stdio ────────────────────────────
51
+ // We can't easily spawn MCP servers here — instead, we proxy through the
52
+ // installed `claude` CLI by calling each server's health_check tool
53
+ // via a one-shot claude -p call with the right env overrides.
54
+
55
+ const results = [];
56
+
57
+ for (const { name, env } of targets) {
58
+ const start = Date.now();
59
+ let healthy = false;
60
+ let error = null;
61
+ let latencyMs = 0;
62
+
63
+ const prompt = JSON.stringify({
64
+ method: 'health_check_proxy',
65
+ server: name,
66
+ });
67
+
68
+ // We use a simpler proxy: just call claude with the env overrides directly.
69
+ // This tests the same path that the MCP tool would use.
70
+ try {
71
+ const envForCall = { ...process.env, ...env };
72
+ const result = execFileSync('claude', [
73
+ '-p', 'Reply with exactly one word: ok',
74
+ '--output-format', 'json',
75
+ '--max-turns', '1',
76
+ ], {
77
+ env: envForCall,
78
+ timeout: TIMEOUT_MS,
79
+ encoding: 'utf8',
80
+ });
81
+ latencyMs = Date.now() - start;
82
+ const parsed = JSON.parse(result);
83
+ healthy = !!(parsed.result || parsed.type === 'result');
84
+ } catch (e) {
85
+ latencyMs = Date.now() - start;
86
+ error = e.code === 'ETIMEDOUT'
87
+ ? `Timed out after ${TIMEOUT_MS}ms`
88
+ : (e.message ?? String(e)).split('\n')[0].slice(0, 120);
89
+ }
90
+
91
+ results.push({ name, healthy, latencyMs, error });
92
+ }
93
+
94
+ // ─── Output ───────────────────────────────────────────────────────────────────
95
+ if (JSON_OUT) {
96
+ console.log(JSON.stringify(results, null, 2));
97
+ } else {
98
+ const green = (s) => `\x1b[32m${s}\x1b[0m`;
99
+ const red = (s) => `\x1b[31m${s}\x1b[0m`;
100
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
101
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
102
+
103
+ console.log(`\n${bold('━━━ MCP ENDPOINT HEALTH CHECK ━━━')}`);
104
+ for (const r of results) {
105
+ const icon = r.healthy ? green('✓') : red('✗');
106
+ const latency = `${r.latencyMs}ms`;
107
+ const status = r.healthy ? green('OK') : red('FAIL');
108
+ const detail = r.error ? dim(` ${r.error}`) : '';
109
+ console.log(` ${icon} ${r.name.padEnd(22)} ${status.padEnd(14)} ${dim(latency)}${detail}`);
110
+ }
111
+ console.log();
112
+
113
+ const unhealthy = results.filter(r => !r.healthy);
114
+ if (unhealthy.length > 0) {
115
+ console.log(red(`${unhealthy.length}/${results.length} servers unhealthy — skip these in quorum:`));
116
+ unhealthy.forEach(r => console.log(` • ${r.name}`));
117
+ console.log();
118
+ }
119
+ }
120
+
121
+ // Exit 1 if any server is unhealthy (useful for scripting)
122
+ const anyUnhealthy = results.some(r => !r.healthy);
123
+ process.exit(anyUnhealthy ? 1 : 0);