@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,173 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-transcript-alloy.cjs
4
+ // Invokes Alloy 6 JAR headless for QGSD transcript scanning spec (GAP-4).
5
+ // Requirements: GAP-4
6
+ //
7
+ // Usage:
8
+ // node bin/run-transcript-alloy.cjs [--spec=<name>]
9
+ //
10
+ // Valid spec names:
11
+ // transcript-scan (default) — GAP-4: transcript scanning invariants
12
+ //
13
+ // Prerequisites:
14
+ // - Java >=17 (https://adoptium.net/)
15
+ // - .planning/formal/alloy/org.alloytools.alloy.dist.jar (see VERIFICATION_TOOLS.md for download)
16
+
17
+ const { spawnSync } = require('child_process');
18
+ const JAVA_HEAP_MAX = process.env.QGSD_JAVA_HEAP_MAX || '512m';
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { writeCheckResult } = require('./write-check-result.cjs');
22
+ const { getRequirementIds } = require('./requirement-map.cjs');
23
+
24
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
25
+ let ROOT = path.join(__dirname, '..');
26
+ for (const arg of process.argv) {
27
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
28
+ }
29
+
30
+ // ── 0. Parse --spec argument ──────────────────────────────────────────────────
31
+ const VALID_SPECS = ['transcript-scan'];
32
+ const args = process.argv.slice(2);
33
+ const specArg = args.find(a => a.startsWith('--spec=')) || null;
34
+ const specName = specArg
35
+ ? specArg.split('=')[1]
36
+ : (args.find(a => !a.startsWith('-')) || 'transcript-scan');
37
+
38
+ if (!VALID_SPECS.includes(specName)) {
39
+ process.stderr.write(
40
+ '[run-transcript-alloy] Unknown spec: ' + specName +
41
+ '. Valid: ' + VALID_SPECS.join(', ') + '\n'
42
+ );
43
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (invalid spec)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
44
+ process.exit(1);
45
+ }
46
+
47
+ // ── 1. Locate Java ───────────────────────────────────────────────────────────
48
+ const JAVA_HOME = process.env.JAVA_HOME;
49
+ let javaExe;
50
+
51
+ if (JAVA_HOME) {
52
+ javaExe = path.join(JAVA_HOME, 'bin', 'java');
53
+ if (!fs.existsSync(javaExe)) {
54
+ process.stderr.write(
55
+ '[run-transcript-alloy] JAVA_HOME is set but java binary not found at: ' + javaExe + '\n' +
56
+ '[run-transcript-alloy] Unset JAVA_HOME or fix the path.\n'
57
+ );
58
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
59
+ process.exit(1);
60
+ }
61
+ } else {
62
+ // Fall back to PATH lookup
63
+ const probe = spawnSync('java', ['--version'], { encoding: 'utf8' });
64
+ if (probe.error || probe.status !== 0) {
65
+ process.stderr.write(
66
+ '[run-transcript-alloy] Java not found. Install Java >=17 and set JAVA_HOME.\n' +
67
+ '[run-transcript-alloy] Download: https://adoptium.net/\n'
68
+ );
69
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (Java not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
70
+ process.exit(1);
71
+ }
72
+ javaExe = 'java';
73
+ }
74
+
75
+ // ── 2. Check Java version >=17 ───────────────────────────────────────────────
76
+ const versionResult = spawnSync(javaExe, ['--version'], { encoding: 'utf8' });
77
+ if (versionResult.error || versionResult.status !== 0) {
78
+ process.stderr.write('[run-transcript-alloy] Failed to run: ' + javaExe + ' --version\n');
79
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (version check failed)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
80
+ process.exit(1);
81
+ }
82
+ const versionOutput = versionResult.stdout + versionResult.stderr;
83
+ // Java version string varies: "openjdk 17.0.1 ..." or "java version \"17.0.1\""
84
+ const versionMatch = versionOutput.match(/(?:openjdk\s+|java version\s+[""]?)(\d+)/i);
85
+ const javaMajor = versionMatch ? parseInt(versionMatch[1], 10) : 0;
86
+ if (javaMajor < 17) {
87
+ process.stderr.write(
88
+ '[run-transcript-alloy] Java >=17 required. Found: ' + versionOutput.split('\n')[0] + '\n' +
89
+ '[run-transcript-alloy] Download Java 17+: https://adoptium.net/\n'
90
+ );
91
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (Java < 17)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
92
+ process.exit(1);
93
+ }
94
+
95
+ // ── 3. Locate org.alloytools.alloy.dist.jar ──────────────────────────────────
96
+ const jarPath = path.join(ROOT, '.planning', 'formal', 'alloy', 'org.alloytools.alloy.dist.jar');
97
+ if (!fs.existsSync(jarPath)) {
98
+ process.stderr.write(
99
+ '[run-transcript-alloy] org.alloytools.alloy.dist.jar not found at: ' + jarPath + '\n' +
100
+ '[run-transcript-alloy] Download Alloy 6.2.0:\n' +
101
+ ' curl -L https://github.com/AlloyTools/org.alloytools.alloy/releases/download/v6.2.0/org.alloytools.alloy.dist.jar \\\n' +
102
+ ' -o .planning/formal/alloy/org.alloytools.alloy.dist.jar\n'
103
+ );
104
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (JAR not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
105
+ process.exit(1);
106
+ }
107
+
108
+ // ── 4. Locate .als file ───────────────────────────────────────────────────────
109
+ const alsPath = path.join(ROOT, '.planning', 'formal', 'alloy', specName + '.als');
110
+ if (!fs.existsSync(alsPath)) {
111
+ process.stderr.write(
112
+ '[run-transcript-alloy] ' + specName + '.als not found at: ' + alsPath + '\n' +
113
+ '[run-transcript-alloy] This file should exist in the repository. Check your git status.\n'
114
+ );
115
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: 0, summary: 'fail: alloy:transcript (ALS not found)', triage_tags: [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
116
+ process.exit(1);
117
+ }
118
+
119
+ // ── 5. Invoke Alloy 6 ────────────────────────────────────────────────────────
120
+ process.stdout.write('[run-transcript-alloy] spec: ' + specName + '\n');
121
+ process.stdout.write('[run-transcript-alloy] ALS: ' + alsPath + '\n');
122
+ process.stdout.write('[run-transcript-alloy] JAR: ' + jarPath + '\n');
123
+
124
+ const _startMs = Date.now();
125
+
126
+ // Use stdio: 'pipe' so we can scan stdout for counterexamples (Alloy exits 0 even on CEX)
127
+ process.stderr.write('[heap] Xms=64m Xmx=' + JAVA_HEAP_MAX + '\n');
128
+ const alloyResult = spawnSync(javaExe, [
129
+ '-Djava.awt.headless=true',
130
+ '-Xms64m', '-Xmx' + JAVA_HEAP_MAX,
131
+ '-jar', jarPath,
132
+ 'exec',
133
+ '--output', '-',
134
+ '--type', 'text',
135
+ '--quiet',
136
+ alsPath,
137
+ ], { encoding: 'utf8', stdio: 'pipe' });
138
+
139
+ if (alloyResult.error) {
140
+ process.stderr.write('[run-transcript-alloy] Alloy invocation failed: ' + alloyResult.error.message + '\n');
141
+ const _runtimeMs = Date.now() - _startMs;
142
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: _runtimeMs, summary: 'fail: alloy:transcript in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
143
+ process.exit(1);
144
+ }
145
+
146
+ // ── 6. Scan stdout for counterexamples ───────────────────────────────────────
147
+ // Alloy 6 exits 0 even when counterexamples are found. Scan stdout to detect them.
148
+ const stdout = alloyResult.stdout || '';
149
+ const stderr = alloyResult.stderr || '';
150
+
151
+ // Write stdout to process.stdout (mirrors stdio: 'inherit' output)
152
+ if (stdout) { process.stdout.write(stdout); }
153
+ if (stderr) { process.stderr.write(stderr); }
154
+
155
+ if (/Counterexample/i.test(stdout)) {
156
+ process.stderr.write(
157
+ '[run-transcript-alloy] WARNING: Counterexample found in ' + specName + '.als assertion.\n' +
158
+ '[run-transcript-alloy] This indicates a spec violation — review .planning/formal/alloy/' + specName + '.als.\n'
159
+ );
160
+ const _runtimeMs = Date.now() - _startMs;
161
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: _runtimeMs, summary: 'fail: alloy:transcript in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
162
+ process.exit(1);
163
+ }
164
+
165
+ if (alloyResult.status !== 0) {
166
+ const _runtimeMs = Date.now() - _startMs;
167
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'fail', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: _runtimeMs, summary: 'fail: alloy:transcript in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
168
+ process.exit(alloyResult.status || 1);
169
+ }
170
+
171
+ const _runtimeMs = Date.now() - _startMs;
172
+ try { writeCheckResult({ tool: 'run-transcript-alloy', formalism: 'alloy', result: 'pass', check_id: 'alloy:transcript', surface: 'alloy', property: 'Hook transcript scanning — boundary detection, tool_use/tool_result pairing uniqueness, ceiling enforcement', runtime_ms: _runtimeMs, summary: 'pass: alloy:transcript in ' + _runtimeMs + 'ms', triage_tags: _runtimeMs > 60000 ? ['timeout-risk'] : [], requirement_ids: getRequirementIds('alloy:transcript'), metadata: { spec: specName } }); } catch (e) { process.stderr.write('[run-transcript-alloy] Warning: failed to write check result: ' + e.message + '\n'); }
173
+ process.exit(0);
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // bin/run-uppaal.cjs
4
+ // Invokes UPPAAL verifyta model checker against .planning/formal/uppaal/quorum-races.xml.
5
+ // Requirements: UPPAAL-01, UPPAAL-02, UPPAAL-03
6
+ //
7
+ // Usage:
8
+ // node bin/run-uppaal.cjs
9
+ // VERIFYTA_BIN=/path/to/verifyta node bin/run-uppaal.cjs
10
+ //
11
+ // Prerequisites:
12
+ // - UPPAAL 4.x or 5.x; set VERIFYTA_BIN to path of the verifyta binary
13
+ // e.g. export VERIFYTA_BIN="$HOME/uppaal/bin/verifyta"
14
+ // - Download: https://uppaal.org/downloads/
15
+ //
16
+ // Graceful degradation: if verifyta is not found, exits 0 with result=inconclusive.
17
+
18
+ const { spawnSync } = require('child_process');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { writeCheckResult } = require('./write-check-result.cjs');
22
+ const { parseNDJSON } = require('./verify-formal-results.cjs');
23
+ const { getRequirementIds } = require('./requirement-map.cjs');
24
+
25
+ // ── Resolve project root (--project-root= overrides __dirname-relative) ─────
26
+ let ROOT = path.join(__dirname, '..');
27
+ for (const arg of process.argv) {
28
+ if (arg.startsWith('--project-root=')) ROOT = path.resolve(arg.slice('--project-root='.length));
29
+ }
30
+
31
+ const CHECK_ID = 'uppaal:quorum-races';
32
+ const SURFACE = 'uppaal';
33
+ const PROPERTY = 'Quorum timed race model — minimum inter-slot gap and maximum timeout for consensus';
34
+ const TAG = '[run-uppaal]';
35
+
36
+ // Default timing fallbacks (override via empirical check-results.ndjson data)
37
+ const DEFAULT_MIN_SLOT_MS = 50;
38
+ const DEFAULT_MAX_SLOT_MS = 500;
39
+ const DEFAULT_TIMEOUT_MS = 1500;
40
+ const DEFAULT_MIN_GAP_MS = 10;
41
+
42
+ /**
43
+ * Read empirical timing bounds from check-results.ndjson.
44
+ * Uses tla:* check records' runtime_ms as slot response time proxy.
45
+ * Falls back to defaults if no data.
46
+ * @returns {{ minSlotMs, maxSlotMs, timeoutMs, minGapMs }}
47
+ */
48
+ function readTimingBounds() {
49
+ const ndjsonPath = process.env.CHECK_RESULTS_PATH ||
50
+ path.join(ROOT, '.planning', 'formal', 'check-results.ndjson');
51
+ const records = parseNDJSON(ndjsonPath);
52
+ const slotRuntimes = records
53
+ .filter(r => r.check_id && r.check_id.startsWith('tla:') && typeof r.runtime_ms === 'number' && r.runtime_ms > 0)
54
+ .map(r => r.runtime_ms);
55
+
56
+ if (slotRuntimes.length === 0) {
57
+ process.stderr.write(TAG + ' No tla: runtime_ms data found — using defaults\n');
58
+ return {
59
+ minSlotMs: DEFAULT_MIN_SLOT_MS,
60
+ maxSlotMs: DEFAULT_MAX_SLOT_MS,
61
+ timeoutMs: DEFAULT_TIMEOUT_MS,
62
+ minGapMs: DEFAULT_MIN_GAP_MS,
63
+ };
64
+ }
65
+
66
+ const minSlotMs = Math.max(1, Math.min(...slotRuntimes));
67
+ const maxSlotMs = Math.max(...slotRuntimes);
68
+ const timeoutMs = Math.max(maxSlotMs * 3, DEFAULT_TIMEOUT_MS);
69
+ const minGapMs = Math.max(1, Math.floor(minSlotMs / 5));
70
+
71
+ process.stderr.write(
72
+ TAG + ' Timing bounds from NDJSON: ' +
73
+ 'min=' + minSlotMs + 'ms, max=' + maxSlotMs + 'ms, timeout=' + timeoutMs + 'ms\n'
74
+ );
75
+ return { minSlotMs, maxSlotMs, timeoutMs, minGapMs };
76
+ }
77
+
78
+ /**
79
+ * Find verifyta binary. Checks VERIFYTA_BIN env, then PATH.
80
+ * Returns null if not found (caller handles graceful degradation).
81
+ */
82
+ function locateVerifyta() {
83
+ const envBin = process.env.VERIFYTA_BIN;
84
+ if (envBin) {
85
+ return fs.existsSync(envBin) ? envBin : null;
86
+ }
87
+ // Check local install from install-formal-tools.cjs
88
+ const localPath = path.join(ROOT, '.planning', 'formal', 'uppaal', 'bin', 'verifyta');
89
+ if (fs.existsSync(localPath)) {
90
+ process.stderr.write(TAG + ' Using local verifyta: ' + localPath + '\n');
91
+ return localPath;
92
+ }
93
+ // Try 'verifyta' on PATH via which
94
+ const which = spawnSync('which', ['verifyta'], { encoding: 'utf8' });
95
+ if (which.status === 0 && which.stdout.trim()) {
96
+ return which.stdout.trim();
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function main() {
102
+ const startMs = Date.now();
103
+ const modelPath = path.join(ROOT, '.planning', 'formal', 'uppaal', 'quorum-races.xml');
104
+ const queryPath = path.join(ROOT, '.planning', 'formal', 'uppaal', 'quorum-races.q');
105
+
106
+ // Check model files exist
107
+ if (!fs.existsSync(modelPath) || !fs.existsSync(queryPath)) {
108
+ const missing = [modelPath, queryPath].filter(p => !fs.existsSync(p)).join(', ');
109
+ process.stderr.write(TAG + ' Model files not found: ' + missing + '\n');
110
+ writeCheckResult({
111
+ tool: 'run-uppaal', formalism: 'uppaal', result: 'inconclusive',
112
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
113
+ runtime_ms: Date.now() - startMs,
114
+ summary: 'inconclusive: model files not found — run Plan 03 first',
115
+ triage_tags: ['missing-model'],
116
+ requirement_ids: getRequirementIds(CHECK_ID),
117
+ metadata: { missing },
118
+ });
119
+ process.exit(0);
120
+ }
121
+
122
+ // Read empirical timing bounds
123
+ const bounds = readTimingBounds();
124
+
125
+ // Locate verifyta
126
+ const verifytaBin = locateVerifyta();
127
+ if (!verifytaBin) {
128
+ process.stderr.write(
129
+ TAG + ' verifyta not found. Install UPPAAL and set VERIFYTA_BIN:\n' +
130
+ TAG + ' export VERIFYTA_BIN="$HOME/uppaal/bin/verifyta"\n' +
131
+ TAG + ' Download: https://uppaal.org/downloads/\n'
132
+ );
133
+ writeCheckResult({
134
+ tool: 'run-uppaal', formalism: 'uppaal', result: 'inconclusive',
135
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
136
+ runtime_ms: Date.now() - startMs,
137
+ summary: 'inconclusive: verifyta not installed — install UPPAAL to run model checker',
138
+ triage_tags: ['no-verifyta'],
139
+ requirement_ids: getRequirementIds(CHECK_ID),
140
+ metadata: {
141
+ min_slot_ms: bounds.minSlotMs,
142
+ max_slot_ms: bounds.maxSlotMs,
143
+ timeout_ms: bounds.timeoutMs,
144
+ },
145
+ });
146
+ process.exit(0);
147
+ }
148
+
149
+ // Build verifyta arguments with empirical timing bounds as -C constants
150
+ const args = [
151
+ '-C', 'MIN_SLOT_MS=' + bounds.minSlotMs,
152
+ '-C', 'MAX_SLOT_MS=' + bounds.maxSlotMs,
153
+ '-C', 'TIMEOUT_MS=' + bounds.timeoutMs,
154
+ '-C', 'MIN_GAP_MS=' + bounds.minGapMs,
155
+ modelPath,
156
+ queryPath,
157
+ ];
158
+
159
+ process.stderr.write(TAG + ' Running: ' + verifytaBin + ' ' + args.join(' ') + '\n');
160
+ const result = spawnSync(verifytaBin, args, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' });
161
+
162
+ const runtimeMs = Date.now() - startMs;
163
+ const combinedOutput = (result.stdout || '') + (result.stderr || '');
164
+
165
+ // Stream output for visibility
166
+ if (result.stdout) process.stdout.write(result.stdout);
167
+ if (result.stderr) process.stderr.write(result.stderr);
168
+
169
+ if (result.error) {
170
+ process.stderr.write(TAG + ' Launch error: ' + result.error.message + '\n');
171
+ writeCheckResult({
172
+ tool: 'run-uppaal', formalism: 'uppaal', result: 'fail',
173
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
174
+ runtime_ms: runtimeMs,
175
+ summary: 'fail: verifyta launch error — ' + result.error.message,
176
+ triage_tags: ['verifyta-error'],
177
+ requirement_ids: getRequirementIds(CHECK_ID),
178
+ metadata: { bounds },
179
+ });
180
+ process.exit(0);
181
+ }
182
+
183
+ // Detect UPPAAL 5.x license requirement (free academic license changed in 5.0)
184
+ if (combinedOutput.includes('License does not cover verifier') ||
185
+ combinedOutput.includes('license key is not set')) {
186
+ process.stderr.write(
187
+ TAG + ' UPPAAL 5.x requires a free academic license key.\n' +
188
+ TAG + ' Register at: https://uppaal.org/academic/ \n' +
189
+ TAG + ' Then set: export UPPAAL_LICENSE_FILE=/path/to/license.key\n'
190
+ );
191
+ writeCheckResult({
192
+ tool: 'run-uppaal', formalism: 'uppaal', result: 'inconclusive',
193
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
194
+ runtime_ms: runtimeMs,
195
+ summary: 'inconclusive: UPPAAL 5.x license required — register at uppaal.org/academic/',
196
+ triage_tags: ['needs-license'],
197
+ requirement_ids: getRequirementIds(CHECK_ID),
198
+ metadata: { bounds },
199
+ });
200
+ process.exit(0);
201
+ }
202
+
203
+ // Detect --disable-memory-reduction duplicate option bug (UPPAAL 5.0.0 bug with -C flags)
204
+ if (combinedOutput.includes('--disable-memory-reduction') &&
205
+ combinedOutput.includes('cannot be specified more than once')) {
206
+ process.stderr.write(
207
+ TAG + ' Known UPPAAL 5.0.0 bug: -C flags trigger duplicate --disable-memory-reduction.\n' +
208
+ TAG + ' Workaround: set constants in the XML model directly instead of via -C.\n'
209
+ );
210
+ writeCheckResult({
211
+ tool: 'run-uppaal', formalism: 'uppaal', result: 'inconclusive',
212
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
213
+ runtime_ms: runtimeMs,
214
+ summary: 'inconclusive: UPPAAL 5.0.0 -C flag bug — falling back to XML-embedded constants',
215
+ triage_tags: ['uppaal-bug'],
216
+ requirement_ids: getRequirementIds(CHECK_ID),
217
+ metadata: { bounds },
218
+ });
219
+ // Retry without -C flags — use the XML-embedded default constants
220
+ process.stderr.write(TAG + ' Retrying without -C flags...\n');
221
+ const retryArgs = [modelPath, queryPath];
222
+ const retry = spawnSync(verifytaBin, retryArgs, { stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' });
223
+ const retryOutput = (retry.stdout || '') + (retry.stderr || '');
224
+ if (retry.stdout) process.stdout.write(retry.stdout);
225
+ if (retry.stderr) process.stderr.write(retry.stderr);
226
+
227
+ // If retry also hits license error, handle that
228
+ if (retryOutput.includes('License does not cover verifier')) {
229
+ process.stderr.write(TAG + ' License required — see above.\n');
230
+ process.exit(0); // already wrote inconclusive check result
231
+ }
232
+
233
+ const retryPassed = retry.status === 0;
234
+ const retryMs = Date.now() - startMs;
235
+ writeCheckResult({
236
+ tool: 'run-uppaal', formalism: 'uppaal',
237
+ result: retryPassed ? 'pass' : 'fail',
238
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
239
+ runtime_ms: retryMs,
240
+ summary: (retryPassed ? 'pass' : 'fail') + ': uppaal:quorum-races in ' + retryMs +
241
+ 'ms (XML defaults, -C workaround)',
242
+ triage_tags: retryPassed ? [] : ['race-detected'],
243
+ requirement_ids: getRequirementIds(CHECK_ID),
244
+ metadata: { bounds, exit_status: retry.status, workaround: 'no-C-flags' },
245
+ });
246
+ process.exit(0);
247
+ }
248
+
249
+ const passed = result.status === 0;
250
+ writeCheckResult({
251
+ tool: 'run-uppaal', formalism: 'uppaal',
252
+ result: passed ? 'pass' : 'fail',
253
+ check_id: CHECK_ID, surface: SURFACE, property: PROPERTY,
254
+ runtime_ms: runtimeMs,
255
+ summary: (passed ? 'pass' : 'fail') + ': uppaal:quorum-races in ' + runtimeMs + 'ms' +
256
+ ' (min_slot=' + bounds.minSlotMs + 'ms, max_slot=' + bounds.maxSlotMs +
257
+ 'ms, timeout=' + bounds.timeoutMs + 'ms)',
258
+ triage_tags: passed ? [] : ['race-detected'],
259
+ requirement_ids: getRequirementIds(CHECK_ID),
260
+ metadata: { bounds, exit_status: result.status },
261
+ });
262
+ }
263
+
264
+ main();
@@ -0,0 +1,134 @@
1
+ 'use strict';
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const SERVICE = 'qgsd';
6
+
7
+ const INDEX_PATH = path.join(os.homedir(), '.claude', 'qgsd-key-index.json');
8
+
9
+ // Read the key index (no keychain access needed — just a JSON file)
10
+ function readIndex() {
11
+ try {
12
+ return new Set(JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8')).accounts || []);
13
+ } catch (_) {
14
+ return new Set();
15
+ }
16
+ }
17
+
18
+ function writeIndex(accounts) {
19
+ fs.mkdirSync(path.dirname(INDEX_PATH), { recursive: true });
20
+ fs.writeFileSync(INDEX_PATH, JSON.stringify({ accounts: [...accounts] }, null, 2));
21
+ }
22
+
23
+ // Check if a key exists — no keychain prompt, reads local index only
24
+ function hasKey(key) {
25
+ return readIndex().has(key);
26
+ }
27
+
28
+ // Lazy-load keytar with a graceful error if the native addon is missing
29
+ function getKeytar() {
30
+ try {
31
+ return require('keytar');
32
+ } catch (e) {
33
+ throw new Error(
34
+ 'keytar native addon not found. Run `npm install keytar` (requires libsecret-dev on Linux).\n' +
35
+ 'Original error: ' + e.message
36
+ );
37
+ }
38
+ }
39
+
40
+ async function set(service, key, value) {
41
+ await getKeytar().setPassword(service, key, value);
42
+ const idx = readIndex();
43
+ idx.add(key);
44
+ writeIndex(idx);
45
+ }
46
+
47
+ async function get(service, key) {
48
+ return getKeytar().getPassword(service, key);
49
+ }
50
+
51
+ async function del(service, key) {
52
+ const result = await getKeytar().deletePassword(service, key);
53
+ const idx = readIndex();
54
+ idx.delete(key);
55
+ writeIndex(idx);
56
+ return result;
57
+ }
58
+
59
+ async function list(service) {
60
+ return getKeytar().findCredentials(service);
61
+ // returns [{account, password}]
62
+ }
63
+
64
+ /**
65
+ * Reads all secrets stored under `service` from the keychain,
66
+ * then patches matching env keys in every mcpServers entry in ~/.claude.json.
67
+ *
68
+ * Algorithm:
69
+ * 1. Load all credentials for the service (account = env var name, password = value)
70
+ * 2. Read ~/.claude.json (parse JSON; if missing/corrupt, log warning and return)
71
+ * 3. Iterate claudeJson.mcpServers — for each server with an `env` block,
72
+ * for each credential whose account name appears as a key in `env`,
73
+ * overwrite `env[account]` with the credential's password.
74
+ * 4. Write the patched JSON back to ~/.claude.json with 2-space indent.
75
+ */
76
+ async function syncToClaudeJson(service) {
77
+ const os = require('os');
78
+ const fs = require('fs');
79
+ const path = require('path');
80
+
81
+ const claudeJsonPath = path.join(os.homedir(), '.claude.json');
82
+
83
+ let credentials;
84
+ try {
85
+ credentials = await list(service);
86
+ } catch (e) {
87
+ process.stderr.write('[qgsd-secrets] keytar unavailable: ' + e.message + '\n');
88
+ return;
89
+ }
90
+
91
+ if (!credentials || credentials.length === 0) return;
92
+
93
+ // Build a lookup map: { ACCOUNT_NAME: password }
94
+ const credMap = {};
95
+ for (const c of credentials) {
96
+ credMap[c.account] = c.password;
97
+ }
98
+
99
+ let raw;
100
+ try {
101
+ raw = fs.readFileSync(claudeJsonPath, 'utf8');
102
+ } catch (e) {
103
+ process.stderr.write('[qgsd-secrets] ~/.claude.json not found, skipping sync\n');
104
+ return;
105
+ }
106
+
107
+ let claudeJson;
108
+ try {
109
+ claudeJson = JSON.parse(raw);
110
+ } catch (e) {
111
+ process.stderr.write('[qgsd-secrets] ~/.claude.json is invalid JSON, skipping sync\n');
112
+ return;
113
+ }
114
+
115
+ if (!claudeJson.mcpServers || typeof claudeJson.mcpServers !== 'object') return;
116
+
117
+ let patched = 0;
118
+ for (const serverName of Object.keys(claudeJson.mcpServers)) {
119
+ const server = claudeJson.mcpServers[serverName];
120
+ if (!server.env || typeof server.env !== 'object') continue;
121
+ for (const envKey of Object.keys(server.env)) {
122
+ if (credMap[envKey] !== undefined) {
123
+ server.env[envKey] = credMap[envKey];
124
+ patched++;
125
+ }
126
+ }
127
+ }
128
+
129
+ if (patched > 0) {
130
+ fs.writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
131
+ }
132
+ }
133
+
134
+ module.exports = { set, get, delete: del, list, hasKey, syncToClaudeJson, SERVICE };